Java 8 有多牛?打破一切你對接口的認(rèn)知!
前段時間面試了一個 39 歲的程序員,結(jié)果不是很理想,沒看過的點(diǎn)擊這里閱讀。
最近也面試一些 Java 程序員,不乏工作 4、5 年經(jīng)驗(yàn)的,當(dāng)我問他一些 Java 8 的新特性時,大多卻答不上來。
比如下面這道題:
棧長:接口里面可以寫方法嗎?
小A:當(dāng)然可以啊,默認(rèn)就是抽象方法。
棧長:那接口里面可以寫實(shí)現(xiàn)方法嗎?
小A:不可以,所有方法必須是抽象的。
棧長:你確定嗎?
小A:確定……
小A看起來對我的問題有點(diǎn)懷疑人生,心里肯定估摸著,我不會在給他埋了什么坑吧。然后他還是仔細(xì)再想了一下,最后還是斬釘截鐵的告訴我:接口里面只能寫抽象方法,不能寫實(shí)現(xiàn)方法。
棧長:接口里面是可以寫實(shí)現(xiàn)方法的,Java 8 開始就可以了,你用過 Java 8 嗎?
小A:好吧,看來是我學(xué)藝不精,Java 8 有了解一點(diǎn),比如那個 Lambda 表達(dá)式,但實(shí)際項(xiàng)目中也沒怎么用。
通過和小A的交流,我也看到了許多開發(fā)者的問題,雖然開發(fā)版本用的是 Java 8,但實(shí)際用的還是 Java 8 之前的最基礎(chǔ)的語法,對 Java 8 新增的特性一無所知。
Java 8 至 2014 年發(fā)布至今,已經(jīng)過了 6 個年頭了,最新的 Java 14 都發(fā)布了,OK,這個不在本篇討論范圍之內(nèi), Java 8+ 系列教程請關(guān)注公眾號Java技術(shù)?;貜?fù) "java" 進(jìn)行閱讀,本篇就是想順著問小A的這個問題展開。
什么是默認(rèn)方法和靜態(tài)方法?
上面也說了,Java 8 開始是可以有方法實(shí)現(xiàn)的,可以在接口中添加默認(rèn)方法和靜態(tài)方法。
默認(rèn)方法用 default 修飾,只能用在接口中,靜態(tài)方法用 static 修飾,這個我們不陌生了。并且接口中的默認(rèn)方法、靜態(tài)方法可以同時有多個。
在接口中寫實(shí)現(xiàn)方法一點(diǎn)也不稀奇,像這樣的用法,從 Java 8 到 Java 14 已是遍地開花,到處都可以看到接口默認(rèn)方法和靜態(tài)方法的身影。
比如我們來看下在 JDK API 中 java.util.Map 關(guān)于接口默認(rèn)方法和靜態(tài)方法的應(yīng)用。
- /*
- * 來源公眾號:Java技術(shù)棧
- */
- public interface Map<K,V> {
- ...
- /**
- * 接口默認(rèn)方法
- */
- default boolean remove(Object key, Object value) {
- Object curValue = get(key);
- if (!Objects.equals(curValue, value) ||
- (curValue == null && !containsKey(key))) {
- return false;
- }
- remove(key);
- return true;
- }
- ...
- /**
- * 接口靜態(tài)方法
- */
- public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
- return (Comparator<Map.Entry<K, V>> & Serializable)
- (c1, c2) -> c1.getKey().compareTo(c2.getKey());
- }
- ...
- }
為什么要有接口默認(rèn)方法?
舉一個很現(xiàn)實(shí)的例子:
我們的接口老早就寫好了,后面因?yàn)楦鞣N業(yè)務(wù)問題,避免不了要修改接口。
在 Java 8 之前,比如要在一個接口中添加一個抽象方法,那所有的接口實(shí)現(xiàn)類都要去實(shí)現(xiàn)這個方法,不然就會編譯錯誤,而某些實(shí)現(xiàn)類根本就不需要實(shí)現(xiàn)這個方法也被迫要寫一個空實(shí)現(xiàn),改動會非常大。
所以,接口默認(rèn)方法就是為了解決這個問題,只要在一個接口添加了一個默認(rèn)方法,所有的實(shí)現(xiàn)類就自動繼承,不需要改動任何實(shí)現(xiàn)類,也不會影響業(yè)務(wù),爽歪歪。
另外,接口默認(rèn)方法可以被接口實(shí)現(xiàn)類重寫。
為什么要有接口靜態(tài)方法?
接口靜態(tài)方法和默認(rèn)方法類似,只是接口靜態(tài)方法不可以被接口實(shí)現(xiàn)類重寫。
接口靜態(tài)方法只可以直接通過靜態(tài)方法所在的 接口名.靜態(tài)方法名 來調(diào)用。
接口默認(rèn)方法多繼承沖突問題
因?yàn)榻涌谀J(rèn)方法可以被繼承并重寫,如果繼承的多個接口都存在相同的默認(rèn)方法,那就存在沖突問題。
下面我會列舉 3 個沖突示例場景。
沖突一
來看下面這段程序:
- /*
- * 來源公眾號:Java技術(shù)棧
- */
- interface People {
- default void eat(){
- System.out.println("人吃飯");
- }
- }
- /*
- * 來源公眾號:Java技術(shù)棧
- */
- interface Man {
- default void eat(){
- System.out.println("男人吃飯");
- }
- }
- /*
- * 來源公眾號:Java技術(shù)棧
- */
- interface Boy extends Man, People {
- }
Boy 同時繼承了 People 和 Man,此時在 IDEA 編輯器中就會報錯:
這就是接口多繼承帶來的沖突問題,Boy 不知道該繼承誰的,這顯然也是個問題,IDEA 也會提示,需要重寫這個方法才能解決問題:
- /*
- * 來源公眾號:Java技術(shù)棧
- */
- interface Boy extends Man, People {
- @Override
- default void eat() {
- System.out.println("男孩吃飯");
- }
- }
在方法里面還能直接調(diào)用指定父接口的默認(rèn)方法,比如:
- /*
- * 來源公眾號:Java技術(shù)棧
- */
- interface Boy extends Man, People {
- @Override
- default void eat() {
- People.super.eat();
- Man.super.eat();
- System.out.println("男孩吃飯");
- }
- }
再加個實(shí)現(xiàn)類測試一下:
- /*
- * 來源公眾號:Java技術(shù)棧
- */
- static class Student implements Boy {
- public static void main(String[] args) {
- Student student = new Student();
- student.eat();
- }
- }
輸出:
- 人吃飯
- 男人吃飯
- 男孩吃飯
嗯,很強(qiáng)大!
沖突二
我們再換一種寫法,把 Man 繼承 People,然后 Man 重寫 People 中的默認(rèn)方法。
此時,編輯器不報錯了,而 People 的默認(rèn)方法置灰了,提示沒有被用到。
再運(yùn)行一下上面的示例,輸出:
男人吃飯
因?yàn)?Man 繼承 People,Man 又重定了默認(rèn)方法。很顯然,這個時候,Boy 知道該繼承誰的默認(rèn)方法了。
沖突三
在 Man 接口中新增一個方法:say,然后在 Boy 接口中新增一個默認(rèn)方法:say。
這時候,Man 中的抽象方法居然被忽略了,IDEA 都提示說沒用到,這顯然是默認(rèn)方法優(yōu)先于抽象方法。
總結(jié)
本文介紹了 Java 8 的默認(rèn)方法和靜態(tài)方法,以及默認(rèn)方法的沖突問題解決方案。所以,大家出去面試時,再也不要說接口不能寫實(shí)現(xiàn)方法了,那就太 OUT 了。。
文中只舉了 3 個默認(rèn)方法的沖突場景,不確定還沒有更多沖突問題。默認(rèn)方法雖然解決了接口變動帶來的問題,但如果設(shè)計不當(dāng),或者過度設(shè)計,其帶來的方法沖突問題也是需要引起注意的。
本文轉(zhuǎn)載自微信公眾號「 Java技術(shù)?!?,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系 Java技術(shù)棧公眾號。