《Java 8 in Action》Chapter 9:默認方法

傳統(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)。

image

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)删性,即行為的多繼承亏娜。這是一種讓類從多個來源重用代碼的能力。


image

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ī)則可以進行判斷。

  1. 類中的方法優(yōu)先級最高烧栋。類或父類中聲明的方法的優(yōu)先級高于任何聲明為默認方法的優(yōu)先級写妥。
  2. 如果無法依據第一條進行判斷,那么子接口的優(yōu)先級更高:函數(shù)簽名相同時审姓,優(yōu)先選擇擁有最具體實現(xiàn)的默認方法的接口珍特,即如果 B 繼承了 A ,那么 B 就比 A 更加具體魔吐。
  3. 最后扎筒,如果還是無法判斷莱找,繼承了多個接口的類必須通過顯式覆蓋和調用期望的方法,顯式地選擇使用哪一個默認方法的實現(xiàn)嗜桌。

4.2 菱形繼承問題

image

了解即可

5. 小結

  1. Java 8中的接口可以通過默認方法和靜態(tài)方法提供方法的代碼實現(xiàn)奥溺。
  2. 默認方法的開頭以關鍵字 default 修飾,方法體與常規(guī)的類方法相同骨宠。
  3. 向發(fā)布的接口添加抽象方法不是源碼兼容的浮定。
  4. 默認方法的出現(xiàn)能幫助庫的設計者以后向兼容的方式演進API。
  5. 默認方法可以用于創(chuàng)建可選方法和行為的多繼承层亿。
  6. 我們有辦法解決由于一個類從多個接口中繼承了擁有相同函數(shù)簽名的方法而導致的沖突桦卒。
  7. 類或者父類中聲明的方法的優(yōu)先級高于任何默認方法。如果前一條無法解決沖突棕所,那就選擇同函數(shù)簽名的方法中實現(xiàn)得最具體的那個接口的方法闸盔。
  8. 兩個默認方法都同樣具體時,你需要在類中覆蓋該方法琳省,顯式地選擇使用哪個接口中提供的默認方法迎吵。

Tips

本文同步發(fā)表在公眾號,歡迎大家關注针贬!??
后續(xù)筆記歡迎關注獲取第一時間更新击费!


公眾號二維碼.jpg
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市桦他,隨后出現(xiàn)的幾起案子蔫巩,更是在濱河造成了極大的恐慌,老刑警劉巖快压,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件圆仔,死亡現(xiàn)場離奇詭異,居然都是意外死亡蔫劣,警方通過查閱死者的電腦和手機坪郭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來脉幢,“玉大人歪沃,你說我怎么就攤上這事∠铀桑” “怎么了沪曙?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長萎羔。 經常有香客問我液走,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任育灸,我火速辦了婚禮腻窒,結果婚禮上,老公的妹妹穿的比我還像新娘磅崭。我一直安慰自己,他們只是感情好瓦哎,可當我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布砸喻。 她就那樣靜靜地躺著,像睡著了一般蒋譬。 火紅的嫁衣襯著肌膚如雪割岛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天犯助,我揣著相機與錄音癣漆,去河邊找鬼。 笑死剂买,一個胖子當著我的面吹牛惠爽,可吹牛的內容都是我干的。 我是一名探鬼主播瞬哼,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼婚肆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了坐慰?” 一聲冷哼從身側響起较性,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎结胀,沒想到半個月后赞咙,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡糟港,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年攀操,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片着逐。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡崔赌,死狀恐怖,靈堂內的尸體忽然破棺而出耸别,到底是詐尸還是另有隱情健芭,我是刑警寧澤,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布秀姐,位于F島的核電站慈迈,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜痒留,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一谴麦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧伸头,春花似錦匾效、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至扫步,卻和暖如春魔策,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背河胎。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工闯袒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人游岳。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓政敢,卻偏偏與公主長得像,于是被迫代替她去往敵國和親吭历。 傳聞我的和親對象是個殘疾皇子堕仔,可洞房花燭夜當晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內容

  • 從技術研發(fā)到產品經理也經過了快半年的時候,最近越來越發(fā)現(xiàn)自己需要學習的東西很多晌区,還好有個產品的“師傅”在無微不至的...
    漿糊大雄閱讀 1,579評論 0 0
  • 我們常常聽到這樣的話:"不成功是你努力的不夠摩骨,放棄是懦弱的表現(xiàn),成功者的字典里沒有放棄這個詞朗若。" 在現(xiàn)實生活中恼五,真...
    雪兆峰年閱讀 408評論 0 0
  • 可能跟教練說了腰腹厚到不能忍,所以今晚重點都在練腰腹上哭懈。 動作1灾馒,道具trx。雙手拉住繩子遣总,拉到繩子緊繃開始深蹲睬罗,...
    三心小姐閱讀 131評論 0 0
  • 親愛的小八不知道那邊的空氣新鮮不新鮮也不知道你有沒有買到喜歡的新番手辦總之我希望我可以在夢中和你相聚與你一起糾結到...
    小菜粥飯閱讀 363評論 1 1