默認方法給JVM的指令集增加了一個非常不錯的新特性。使用了默認方法之後,如果庫中的接口增加了新的方法,實現了這個接口的用戶類能夠自動獲得這個方法的默認實現。一旦用戶想更新他的實現類的話,只需覆蓋一下這個默認方法就可以了,取而代之的是一個在特定場景下更有意義的實現。更棒的是,用戶可以在重寫的方法裡面調用接口的默認實現來增加一些額外的功能。
目前為止一切都還不錯。然而,給現有的Java接口增加默認方法可能會導致代碼的不兼容。看個例子就很容易能明白了。假設有一個庫,它需要用戶實現它的一個接口作為輸入:
interface SimpleInput { void foo(); void bar();} abstract class SimpleInputAdapter implements SimpleInput { @Override public void bar() { // some default behavior ... }}在Java 8以前,上述這種接口和一個對應的適配器類的組合在Java語言中是一種很常見的模式。類庫的開發人員提供了一個適配器來減少庫使用者的編碼量。然而提供這個接口的目的其實是為了能實現某種類似多重繼承的關係。
我們假設有一個用戶使用了這個適配器:
class MyInput extends SimpleInputAdapter{ @Override public void foo() { // do something ... } @Override public void bar() { super.bar(); // do something additionally ... }}有了這個實現,用戶可以和庫進行交互了。注意這個實現是如何重寫bar方法來給默認的實現增加額外的功能的。
那如果這個庫遷移到Java 8的話會怎樣?首先,這個庫很可能會廢棄掉這個適配器類並將這個功能遷移到默認方法裡。最終這個接口看起來會是這樣的:
interface SimpleInput { void foo(); default void bar() { // some default behavior }}}有了這個新接口後,用戶得更新他的代碼來使用這個默認方法,而不再是適配器類了。使用新接口而非適配器類的一大好處就是,用戶可以去繼承一個別的類而不是這個適配器類了。我們來動手實踐一下,將MyInput類改造成使用默認方法。由於現在我們可以繼承別的類了,我們再額外地擴展一個第三方的基類試試。這個基類具體是做什麼的在這裡並不重要,我們先假設一下這麼做對我們這個用例來說是有意義的。
class MyInput extends ThirdPartyBaseClass implements SimpleInput { @Override public void foo() { // do something ... } @Override public void bar() { SimpleInput.super.foo(); // do something additionally ... }}為了實現和原先那個類同樣的功能,這裡我們用到了Java 8的新語法來調用接口的默認方法。同樣的,我們把myMethod的邏輯放到某個基類MyBase裡面。可以搥搥肩膀放鬆下了。重構之後棒極了!
我們使用的這個庫得到了很大的改進。然而,維護人員需要添加另一個接口來實現一些額外的功能。這個接口叫做CompexInput ,它繼承了SimpleInput類,並增加了一個額外的方法。由於通常都認為默認方法是可以放心地添加的,因此維護人員重寫了SimpleInput類的默認方法並添加了一些額外的動作來給用戶提供一個更好的默認實現。畢竟使用適配器類的時候這個做法也十分常見:
interface ComplexInput extends SimpleInput { void qux(); @Override default void bar() { SimpleInput.super.bar(); // so complex, we need to do more ... }}這個新特性看起來非常不錯,因此ThirdPartyBaseClass類的維護人員也決定使用這個庫了。為了實現這個,他將ThirdPartyBaseClass類實現了ComplexInput接口。
但這樣的話對MyInput類意味著什麼?由於它繼承了ThirdPartyBaseClass類,因此默認實現了ComplexInput接口,這樣的話調用SimpleInput的默認方法就不合法了。結果就是,用戶的代碼最後無法通過編譯。還有就是,現在已經徹底無法調用這個方法了,因為Java把這種調用間接父類的super-super方法認為是不合法的。你只能去調用ComplexInput接口的默認方法了。然而這首先需要你在MyInput類中顯式的實現一下這個接口。對於這個庫的用戶而言,這些改動完全是意想不到的。
(注:簡單點說其實就是:
interface A { default void test() { }}interface B extends A { default void test() { }}public class Test implements B { public void test() { B.super.test(); //A.super.test(); 錯誤}}當然這麼寫的話是用戶主動選擇實現了B接口,而文中的例子由於引入了一個基類,因此由於庫和基類中都進行了一個看似沒有影響的改動,實際上卻導致用戶代碼無法通過編譯)
很奇怪的是,Java在運行時並沒有對這個進行區分。 JVM的校驗器允許一個編譯過的類進行SimpleInput::foo方法的調用,儘管加載的這個類繼承了ThirdPartyBaseClass的更新版本後隱式地實現了ComplexInput接口。要怪只能怪編譯器了。 (注:編譯器與運行時的行為不一致)
那我們從中學到了什麼?簡單地說,不要在另一個接口中重寫原接口的默認方法。不要用另一個默認方法來重寫它,也不要某個抽象方法來重寫它。總而言之,使用默認方法時應當十分謹慎。雖然它們使得Java現有的集合庫的接口更容易改進了,但它允許你在類的繼承結構中進行方法調用,這本質上其實是增加了複雜性。在Java 7以前,你只需遍歷線性的類層次結構看一下實際調用的代碼就可以了。當你覺得的確需要的時候,再去使用默認方法。
以上就是針對為什麼要慎用Java8的默認方法進行的詳細解釋,希望對大家的學習有所幫助。