傳統(tǒng)上,Java程序的接口是將相關方法按照約定組合到一起的方式。實現(xiàn)接口的類必須為接口中定義的每個方法提供一個實現(xiàn)辙售,或者從父類中繼承它的實現(xiàn)量淌。
但是骗村,一旦類庫的設計者需要更新接口,向其中加入新的方法呀枢,這種方式就會出現(xiàn)問題∨吖桑現(xiàn)實情況是,現(xiàn)存的實體類往往不在接口設計者的控制范圍之內裙秋,這些實體類為了適配新的接口約定也需要進行修改琅拌。
由于Java 8的API在現(xiàn)存的接口上引入了非常多的新方法,這種變化帶來的問題也愈加嚴重摘刑,一個例子就是前幾章中使用過的 List 接口上的 sort 方法进宝。
想象一下其他備選集合框架的維護人員會多么抓狂吧,像Guava和Apache Commons這樣的框架現(xiàn)在都需要修改實現(xiàn)了 List 接口的所有類枷恕,為其添加sort 方法的實現(xiàn)党晋。
Java 8為了解決這一問題引入了一種新的機制。Java 8中的接口現(xiàn)在支持在聲明方法的同時提供實現(xiàn),通過兩種方式可以完成這種操作未玻。其一灾而,Java 8允許在接口內聲明靜態(tài)方法。
其二扳剿,Java 8引入了一個新功能旁趟,叫默認方法,通過默認方法你可以指定接口方法的默認實現(xiàn)庇绽。換句話說轻庆,接口能提供方法的具體實現(xiàn)。因此敛劝,實現(xiàn)接口的類如果不顯式地提供該方法的具體實現(xiàn)余爆,
就會自動繼承默認的實現(xiàn)。這種機制可以使你平滑地進行接口的優(yōu)化和演進夸盟。實際上蛾方,到目前為止你已經使用了多個默認方法。兩個例子就是你前面已經見過的 List 接口中的 sort 上陕,以及 Collection 接口中的 stream 桩砰。
第1章中 List 接口中的 sort 方法是Java 8中全新的方法,它的定義如下:
default void sort(Comparator<? super E> c){
Collections.sort(this, c);
}
請注意返回類型之前的新 default 修飾符释簿。通過它亚隅,我們能夠知道一個方法是否為默認方法。這里 sort 方法調用了 Collections.sort 方法進行排序操作庶溶。由于有了這個新的方法煮纵,我們現(xiàn)在可以直接通過調用 sort ,對列表中的元素進行排序偏螺。
List<Integer> numbers = Arrays.asList(3, 5, 1, 2, 6);
numbers.sort(Comparator.naturalOrder());
不過除此之外行疏,這段代碼中還有些其他的新東西。我們調用了Comparator.naturalOrder 方法套像。這是 Comparator 接口的一個全新的靜態(tài)方法酿联,它返回一個Comparator 對象,并按自然序列對其中的元素進行排序(即標準的字母數(shù)字方式排序)夺巩。
第4章中的 Collection 中的 stream 方法的定義如下:
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
我們在之前的幾章中大量使用了該方法來處理集合贞让,這里 stream 方法中調用了SteamSupport.stream 方法來返回一個流。你注意到 stream 方法的主體是如何調用 spliterator 方法的了嗎柳譬?它也是 Collection 接口的一個默認方法喳张。
接口和抽象類還是有一些本質的區(qū)別,我們在這一章中會針對性地進行討論征绎。
簡而言之蹲姐,向接口添加方法是諸多問題的罪惡之源磨取;一旦接口發(fā)生變化,實現(xiàn)這些接口的類往往也需要更新柴墩,提供新添方法的實現(xiàn)才能適配接口的變化忙厌。如果你對接口以及它所有相關的實現(xiàn)有完全的控制,這可能不是個大問題江咳。但是這種情況是極少的逢净。這就是引入默認方法的目的:它讓類可以自動地繼承接口的一個默認實現(xiàn)。
1. 不斷演進的 API
1.1 初始版本的 API
Resizable 接口的最初版本提供了下面這些方法:
public interface Drawable {
void draw();
}
public interface Resizable extends Drawable {
int getWidth();
void setWidth(int width);
int getHeight();
void setHeight(int height);
void setAbsoluteSize(int width, int height);
}
用戶根據自身的需求實現(xiàn)了 Resizable 接口歼指,創(chuàng)建了 Ellipse 類:
public class Ellipse implements Resizable {
...
}
他實現(xiàn)了一個處理各種 Resizable 形狀(包括 Ellipse )的游戲:
public class Square implements Resizable {
...
}
public class Triangle implements Resizable {
...
}
public class Game {
public static void main(String[] args) {
List<Resizable> resizableShapes =
Arrays.asList(new Square(), new Triangle(), new Ellipse());
Utils.paint(resizableShapes);
}
}
public class Utils {
public static void paint(List<Resizable> list) {
list.forEach(r -> {
r.setAbsoluteSize(42, 42);
r.draw();
});
}
}
1.2 第二版 API
庫上線使用幾個月之后爹土,你收到很多請求,要求你更新 Resizable 的實現(xiàn)踩身,讓 Square Triangle 以及其他的形狀都能支持 setRelativeSize 方法胀茵。為了滿足這些新的需求,你發(fā)布了第二版API挟阻。
public interface Resizable extends Drawable {
int getWidth();
void setWidth(int width);
int getHeight();
void setHeight(int height);
void setAbsoluteSize(int width, int height);
void setRelativeSize(int wFactor, int hFactor);
}
對 Resizable 接口的更新導致了一系列的問題琼娘。首先,接口現(xiàn)在要求它所有的實現(xiàn)類添加setRelativeSize 方法的實現(xiàn)附鸽。但是用戶最初實現(xiàn)的 Ellipse 類并未包含 setRelativeSize方法脱拼。向接口添加新方法是二進制兼容的,這意味著如果不重新編譯該類坷备,即使不實現(xiàn)新的方法熄浓,現(xiàn)有類的實現(xiàn)依舊可以運行。不過省撑,用戶可能修改他的游戲赌蔑,在他的 Utils.paint 方法中調用setRelativeSize 方法,因為 paint 方法接受一個 Resizable 對象列表作為參數(shù)丁侄。如果傳遞的是一個 Ellipse 對象惯雳,程序就會拋出一個運行時錯誤,因為它并未實現(xiàn) setRelativeSize 方法:
Exception in thread "main" java.lang.AbstractMethodError:lambdasinaction.chap9.Ellipse.setRelativeSize(II)V
其次鸿摇,如果用戶試圖重新編譯整個應用(包括 Ellipse 類),他會遭遇下面的編譯錯誤:
Error:(9, 8) java: com.lujiahao.learnjava8.chapter9.Ellipse不是抽象的, 并且未覆蓋
com.lujiahao.learnjava8.chapter9.Resizable中的抽象方法setRelativeSize(int,int)
這就是默認方法試圖解決的問題劈猿。它讓類庫的設計者放心地改進應用程序接口拙吉,無需擔憂對遺留代碼的影響,這是因為實現(xiàn)更新接口的類現(xiàn)在會自動繼承一個默認的方法實現(xiàn)揪荣。
變更對Java程序的影響大體可以分成三種類型的兼容性筷黔,分別是:
- 二進制級的兼容
- 源代碼級的兼容
- 函數(shù)行為的兼容
2. 概述默認方法
默認方法由 default 修飾符修飾,并像類中聲明的其他方法一樣包含方法體仗颈。比如佛舱,你可以像下面這樣在集合庫中定義一個名為Sized 的接口椎例,在其中定義一個抽象方法 size ,以及一個默認方法 isEmpty :
public interface Sized {
int size();
default boolean isEmpty() {
return size() == 0;
}
}
這樣任何一個實現(xiàn)了 Sized 接口的類都會自動繼承 isEmpty 的實現(xiàn)请祖。因此订歪,向提供了默認實現(xiàn)的接口添加方法就不是源碼兼容的。
默認方法在Java 8的API中已經大量地使用了肆捕。本章已經介紹過我們前一章中大量使用的 Collection 接口的 stream 方法就是默認方法刷晋。 List 接口的 sort 方法也是默認方法。第3章介紹的很多函數(shù)式接口慎陵,比如 Predicate 眼虱、 Function 以及 Comparator 也引入了新的默認方法,比如 Predicate.and 或者 Function.andThen (記住席纽,函數(shù)式接口只包含一個抽象方法捏悬,默認方法是種非抽象方法)。
3. 默認方法的使用模式
3.1 可選方法
類實現(xiàn)了接口润梯,不過卻刻意地將一些方法的實現(xiàn)留白过牙。我們以Iterator 接口為例來說嫁审。 Iterator 接口定義了 hasNext 丙曙、 next ,還定義了 remove 方法汗菜。Java 8之前彤蔽,由于用戶通常不會使用該方法摧莽, remove 方法常被忽略。因此顿痪,實現(xiàn) Interator 接口的類通常會為 remove 方法放置一個空的實現(xiàn)镊辕,這些都是些毫無用處的模板代碼。采用默認方法之后蚁袭,你可以為這種類型的方法提供一個默認的實現(xiàn)征懈,這樣實體類就無需在自己的實現(xiàn)中顯式地提供一個空方法。比如揩悄,在Java 8中卖哎, Iterator 接口就為 remove 方法提供了一個默認實現(xiàn),如下所示:
public interface Iterator<E> {
...
default void remove() {
throw new UnsupportedOperationException("remove");
}
...
}
3.2 行為的多繼承
默認方法讓之前無法想象的事兒以一種優(yōu)雅的方式得以實現(xiàn)删性,即行為的多繼承亏娜。這是一種讓類從多個來源重用代碼的能力。
Java的類只能繼承單一的類蹬挺,但是一個類可以實現(xiàn)多接口维贺。要確認也很簡單,下面是Java API中對 ArrayList 類的定義:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
}
3.2.1 類型的多繼承
這個例子中 ArrayList 繼承了一個類巴帮,實現(xiàn)了六個接口溯泣。因此 ArrayList 實際是七個類型的直接子類虐秋,分別是: AbstractList 、 List 垃沦、 RandomAccess 客给、 Cloneable 、 Serializable 栏尚、Iterable 和 Collection 起愈。所以,在某種程度上译仗,我們早就有了類型的多繼承抬虽。
由于Java 8中接口方法可以包含實現(xiàn),類可以從多個接口中繼承它們的行為(即實現(xiàn)的代碼)纵菌。讓我們從一個例子入手阐污,看看如何充分利用這種能力來為我們服務。保持接口的精致性和正交性能幫助你在現(xiàn)有的代碼基上最大程度地實現(xiàn)代碼復用和行為組合咱圆。
3.2.2 利用正交方法的精簡接口
假設你需要為你正在創(chuàng)建的游戲定義多個具有不同特質的形狀笛辟。有的形狀需要調整大小,但是不需要有旋轉的功能序苏;有的需要能旋轉和移動手幢,但是不需要調整大小。這種情況下忱详,你怎么設計才能盡可能地重用代碼围来?
你可以定義一個單獨的 Rotatable 接口,并提供兩個抽象方法 setRotationAngle 和getRotationAngle 匈睁,如下所示:
public interface Rotatable {
int getRotationAngle();
void setRotationAngle(int angleInDegrees);
default void rotateBy(int angleInDegrees) {
setRotationAngle((getRotationAngle() + angleInDegrees) % 360);
}
}
這種方式和模板設計模式有些相似监透,都是以其他方法需要實現(xiàn)的方法定義好框架算法。
現(xiàn)在航唆,實現(xiàn)了 Rotatable 的所有類都需要提供 setRotationAngle 和 getRotationAngle的實現(xiàn)胀蛮,但與此同時它們也會天然地繼承 rotateBy 的默認實現(xiàn)。
類似地糯钙,你可以定義之前看到的兩個接口 Moveable 和 Resizable 粪狼。它們都包含了默認實現(xiàn)。下面是 Moveable 的代碼:
public interface Moveable {
int getX();
void setX(int x);
int getY();
void setY(int y);
default void moveHorizontally(int distance) {
setX(getX() + distance);
}
default void moveVertically(int distance) {
setY(getY() + distance);
}
}
下面是 Resizable 的代碼:
public interface Resizable extends Drawable {
int getWidth();
void setWidth(int width);
int getHeight();
void setHeight(int height);
void setAbsoluteSize(int width, int height);
default void setRelativeSize(int wFactor, int hFactor){
setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor);
}
}
3.2.3 組合接口
通過組合這些接口任岸,你現(xiàn)在可以為你的游戲創(chuàng)建不同的實體類鸳玩。比如, Monster 可以移動演闭、旋轉和縮放。
public class Monster implements Rotatable, Moveable, Resizable {
...
}
Monster 類會自動繼承 Rotatable 颓帝、 Moveable 和 Resizable 接口的默認方法米碰。這個例子中窝革,Monster 繼承了 rotateBy 、 moveHorizontally 吕座、 moveVertically 和 setRelativeSize 的實現(xiàn)虐译。
你現(xiàn)在可以直接調用不同的方法:
Monster m = new Monster();
m.rotateBy(180);
m.moveVertically(10);
像你的游戲代碼那樣使用默認實現(xiàn)來定義簡單的接口還有另一個好處。假設你需要修改moveVertically 的實現(xiàn)吴趴,讓它更高效地運行漆诽。你可以在 Moveable 接口內直接修改它的實現(xiàn),所有實現(xiàn)該接口的類會自動繼承新的代碼(這里我們假設用戶并未定義自己的方法實現(xiàn))锣枝。
通過前面的介紹厢拭,你已經了解了默認方法多種強大的使用模式。不過也可能還有一些疑惑:如果一個類同時實現(xiàn)了兩個接口撇叁,這兩個接口恰巧又提供了同樣的默認方法簽名供鸠,這時會發(fā)生什么情況?類會選擇使用哪一個方法陨闹?這些問題楞捂,我們會在接下來的一節(jié)進行討論。
4. 解決沖突的規(guī)則
隨著默認方法在Java 8中引入趋厉,有可能出現(xiàn)一個類繼承了多個方法而它們使用的卻是同樣的函數(shù)簽名寨闹。這種情況下,類會選擇使用哪一個函數(shù)君账?接下來的例子主要用于說明容易出問題的場景繁堡,并不表示這些場景在實際開發(fā)過程中會經常發(fā)生。
public interface A {
default void hello() {
System.out.println("Hello from A");
}
}
public interface B extends A {
default void hello() {
System.out.println("Hello from B");
}
}
public class C implements A, B {
public static void main(String[] args) {
// 猜猜打印的是什么杈绸?
new C().hello();
}
}
此外帖蔓,你可能早就對C++語言中著名的菱形繼承問題有所了解,菱形繼承問題中一個類同時繼承了具有相同函數(shù)簽名的兩個方法瞳脓。到底該選擇哪一個實現(xiàn)呢塑娇? Java 8也提供了解決這個問題的方案。請接著閱讀下面的內容劫侧。
4.1 解決問題的三條規(guī)則
如果一個類使用相同的函數(shù)簽名從多個地方(比如另一個類或接口)繼承了方法埋酬,通過三條規(guī)則可以進行判斷。
- 類中的方法優(yōu)先級最高烧栋。類或父類中聲明的方法的優(yōu)先級高于任何聲明為默認方法的優(yōu)先級写妥。
- 如果無法依據第一條進行判斷,那么子接口的優(yōu)先級更高:函數(shù)簽名相同時审姓,優(yōu)先選擇擁有最具體實現(xiàn)的默認方法的接口珍特,即如果 B 繼承了 A ,那么 B 就比 A 更加具體魔吐。
- 最后扎筒,如果還是無法判斷莱找,繼承了多個接口的類必須通過顯式覆蓋和調用期望的方法,顯式地選擇使用哪一個默認方法的實現(xiàn)嗜桌。
4.2 菱形繼承問題
了解即可
5. 小結
- Java 8中的接口可以通過默認方法和靜態(tài)方法提供方法的代碼實現(xiàn)奥溺。
- 默認方法的開頭以關鍵字 default 修飾,方法體與常規(guī)的類方法相同骨宠。
- 向發(fā)布的接口添加抽象方法不是源碼兼容的浮定。
- 默認方法的出現(xiàn)能幫助庫的設計者以后向兼容的方式演進API。
- 默認方法可以用于創(chuàng)建可選方法和行為的多繼承层亿。
- 我們有辦法解決由于一個類從多個接口中繼承了擁有相同函數(shù)簽名的方法而導致的沖突桦卒。
- 類或者父類中聲明的方法的優(yōu)先級高于任何默認方法。如果前一條無法解決沖突棕所,那就選擇同函數(shù)簽名的方法中實現(xiàn)得最具體的那個接口的方法闸盔。
- 兩個默認方法都同樣具體時,你需要在類中覆蓋該方法琳省,顯式地選擇使用哪個接口中提供的默認方法迎吵。
Tips
本文同步發(fā)表在公眾號,歡迎大家關注针贬!??
后續(xù)筆記歡迎關注獲取第一時間更新击费!