Java8新增特性,可以為接口中添加默認方法,實現這個接口的所有類都會繼承這個方法,這樣看起來,接口和類的界限就有點不明顯了,同時也會帶來多繼承,菱形問題。這樣設計的初衷是什么?
重所周知,java8開始支持lambda表達式,可以把函數當做參數傳遞,最明顯的lambda表達式應用場景莫過於對collection的每一個元素應用lambda。如果想為Collection實現lambda表達式:list.forEach(…); // 這就是lambda代碼
首先想到的是為Collection的父接口iterator添加抽象方法forEach()。然而,對於已經發布的版本,是沒法在給接口添加新方法的同時不影響已有的實現。如果添加了,那么所有的iterator()實現類都要重寫這個方法,如果只是jre自己的類庫還好說,大量的第三方類庫都使用到了java的優秀集合框架,如果都要重寫,這是不合理的。
因此,如果在Java 8里使用lambda的時候,因為向前兼容的原因而不能用於collection庫,那有多糟糕啊。
由於上述原因,引入了一個新的概念。虛擬擴展方法,也即通常說的defender方法, 現在可以將其加入到接口,這樣可以提供聲明的行為的默認實現。
簡單的說,Java的接口現在可以實現方法了。默認方法帶來的好處是可以為接口添加新的默認方法,而不會破壞接口的實現。
之前:Java接口純粹是契約的集合,是一種程序設計的表達方式。從數據抽象的角度看,能夠在不定義class的同時又可以定義type,將是程序設計中強大而有用的機制。Java接口就是這些純粹的接口組成的數據抽象。Java接口只能夠擁有抽象方法,它不涉及任何實現,也不能創建其對象(這一點和抽象類一致)。
多重繼承模型導致額外的復雜性,其中最著名的是鑽石問題或者叫“討嫌的菱形派生”(Dreadful Diamond onDerivation、DDD)。為什么Java接口能夠避免多繼承的復雜性,關鍵在於它僅僅包含abstract方法。然而從設計的角度看,Java接口放棄了多繼承的內在/固有目標,而顯得是一個權宜之計。
現在:Java8之前,接口不能升級。因為在接口中添加一個方法,會導致老版本接口的所有實現類的中斷。λ表達式作為核心出現,為了配合λ表達式,JDK中Collection庫需要添加新的方法,如forEach(),stream()等,於是引入了默認方法(defender methods,Virtual extension methods)。它是庫/框架設計的程序員的后悔葯。對於以前的遺留代碼,大家都不知道有這個新方法,既不會調用,也不會去實現,如同不存在;編寫新代碼的程序員可以將它視為保底的方法體。類型層次中任何符合override規則的方法,優先於默認方法,因為遺留代碼可能正好有同樣的方法存在。
默認方法,理論上抹殺了Java接口與抽象類的本質區別——前者是契約的集合,后者是接口與實現的結合體。當然,語法上兩者的差別和以前一樣。這就需要程序員來自覺維護兩者的本質區別,把默認方法作為庫、框架向前兼容的手段。
默認方法的一個好處:多繼承的著名的是鑽石問題(The Diamond Problem )再次需要關注。因而使以前某些人認為的“為了解決多繼承問題而引入接口機制”的說法變成明顯的錯誤——以前也是錯誤的認識。
默認方法的聲明很簡單,直接在接口中把方法聲明為default,之后再寫方法的實現即可。這樣所有的實現類都會繼承這個方法,問題是他帶來的多繼承問題如何解決?
Iterable中的默認方法:
default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } }
下面是一個簡單的默認方法實現:
public interface A { default void foo(){ System.out.println("Calling A.foo()"); } } public class Clazz implements A { } Clazz clazz = new Clazz(); clazz.foo(); // 調用A.foo()
下面是一個多繼承:
public interface A { default void foo(){ System.out.println("Calling A.foo()"); } } public interface B { default void foo(){ System.out.println("Calling B.foo()"); } } public class Clazz implements A, B { }
這段代碼不能編譯 有以下原因:
java:class Clazz 從types A到B給foo()繼承了不相關的默認值
為了修復這個,在Clazz里我們不得不手動解決通過重寫沖突的方法,或者調用某個接口中的方法:
public class Clazz implements A, B { public void foo(){} } public class Clazz implements A, B { public void foo(){ A.super.foo(); } }
鑽石問題
情況一:接口IA有子接口IB1、IB2,而類C implements IB1,IB2
package java8; public class C implements IB1, IB2{ public static void main(String[] a) { new C().m(); } }
(1)如果僅僅A定義默認方法m(),執行IA的默認方法;
(2)如果只有IB1、IB2其中一個override IA定義的m(),調用“最具體接口”默認方法;
(3)如果IB1、IB2都override IA定義的m(),而C不提供自己的方法體,則編譯錯誤!因為 “最具體接口”默認方法有兩個。此時,C若Override m(),可以消去編譯錯誤。
(4)類C提供自己的方法體時,可以提供自己的代碼,也可以指定調用C implements 的直接父接口的默認方法
小結:多個接口提供默認方法,則“最具體接口”默認方法勝出,但是不得出現多個“最具體接口”。
情況二:接口IA(或IB1)定義了默認方法m(),類A1相同的方法m(),類C是它們的子類型。
如果類A1提供了實現,按照a simple rule: “the superclass always wins.”),父類的方法 被調用;
如果類A1不提供實現,即A1中m()為抽象方法,仍然按照the superclass always wins.類C需要override m(),給出自己的實現。否則,要么 C聲明為抽象類,要么編譯錯誤。
小結:父類有默認方法的等價物,則默認方法如同不存在。
默認方法是對Java語言的有趣補充 – 你可以把他們看做是lambdas表達式和JDK庫之間的橋梁。默認表達式的主要目標是使標准JDK接口得以進化,並且當我們最終開始使用Java 8的lambdas表達式時,提供給我們一個平滑的過渡體驗。誰知道呢,也許將來我們會在API設計中看到更多的默認方法的應用。
http://www.oschina.net/translate/java-8-explained-default-methods
