1. 程式人生 > >《Java 8 in Action》Chapter 9:預設方法

《Java 8 in Action》Chapter 9:預設方法

傳統上,Java程式的介面是將相關方法按照約定組合到一起的方式。實現介面的類必須為介面中定義的每個方法提供一個實現,或者從父類中繼承它的實現。 但是,一旦類庫的設計者需要更新介面,向其中加入新的方法,這種方式就會出現問題。現實情況是,現存的實體類往往不在介面設計者的控制範圍之內,這些實體類為了適配新的介面約定也需要進行修改。 由於Java 8的API在現存的介面上引入了非常多的新方法,這種變化帶來的問題也愈加嚴重,一個例子就是前幾章中使用過的 List 介面上的 sort 方法。 想象一下其他備選集合框架的維護人員會多麼抓狂吧,像Guava和Apache Commons這樣的框架現在都需要修改實現了 List 介面的所有類,為其新增sort 方法的實現。 Java 8為了解決這一問題引入了一種新的機制。Java 8中的介面現在支援在宣告方法的同時提供實現,通過兩種方式可以完成這種操作。其一,Java 8允許在介面內宣告靜態方法。 其二,Java 8引入了一個新功能,叫預設方法,通過預設方法你可以指定介面方法的預設實現。換句話說,介面能提供方法的具體實現。因此,實現介面的類如果不顯式地提供該方法的具體實現, 就會自動繼承預設的實現。這種機制可以使你平滑地進行介面的優化和演進。實際上,到目前為止你已經使用了多個預設方法。兩個例子就是你前面已經見過的 List 介面中的 sort ,以及 Collection 介面中的 stream 。

第1章中 List 介面中的 sort 方法是Java 8中全新的方法,它的定義如下:

default void sort(Comparator<? super E> c){
    Collections.sort(this, c);
}

請注意返回型別之前的新 default 修飾符。通過它,我們能夠知道一個方法是否為預設方法。這裡 sort 方法呼叫了 Collections.sort 方法進行排序操作。由於有了這個新的方法,我們現在可以直接通過呼叫 sort ,對列表中的元素進行排序。

List<Integer> numbers = Arrays.asList(3, 5, 1, 2, 6);
numbers.sort(Comparator.naturalOrder());

不過除此之外,這段程式碼中還有些其他的新東西。我們呼叫了Comparator.naturalOrder 方法。這是 Comparator 介面的一個全新的靜態方法,它返回一個Comparator 物件,並按自然序列對其中的元素進行排序(即標準的字母數字方式排序)。 第4章中的 Collection 中的 stream 方法的定義如下:

default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
}

我們在之前的幾章中大量使用了該方法來處理集合,這裡 stream 方法中呼叫了SteamSupport.stream 方法來返回一個流。你注意到 stream 方法的主體是如何呼叫 spliterator 方法的了嗎?它也是 Collection 介面的一個預設方法。 介面和抽象類還是有一些本質的區別,我們在這一章中會針對性地進行討論。 簡而言之,向介面新增方法是諸多問題的罪惡之源;一旦介面發生變化,實現這些介面的類往往也需要更新,提供新添方法的實現才能適配介面的變化。如果你對介面以及它所有相關的實現有完全的控制,這可能不是個大問題。但是這種情況是極少的。這就是引入預設方法的目的:它讓類可以自動地繼承介面的一個預設實現。

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);
}
使用者根據自身的需求實現了 Resizable 介面,建立了 Ellipse 類:
public class Ellipse implements Resizable {
    ...
}
他實現了一個處理各種 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 的實現,讓 Square Triangle 以及其他的形狀都能支援 setRelativeSize 方法。為了滿足這些新的需求,你釋出了第二版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 介面的更新導致了一系列的問題。首先,介面現在要求它所有的實現類新增setRelativeSize 方法的實現。但是使用者最初實現的 Ellipse 類並未包含 setRelativeSize方法。向介面新增新方法是二進位制相容的,這意味著如果不重新編譯該類,即使不實現新的方法,現有類的實現依舊可以執行。不過,使用者可能修改他的遊戲,在他的 Utils.paint 方法中呼叫setRelativeSize 方法,因為 paint 方法接受一個 Resizable 物件列表作為引數。如果傳遞的是一個 Ellipse 物件,程式就會丟擲一個執行時錯誤,因為它並未實現 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)

這就是預設方法試圖解決的問題。它讓類庫的設計者放心地改進應用程式介面,無需擔憂對遺留程式碼的影響,這是因為實現更新介面的類現在會自動繼承一個預設的方法實現。

變更對Java程式的影響大體可以分成三種類型的相容性,分別是:

  • 二進位制級的相容
  • 原始碼級的相容
  • 函式行為的相容

2. 概述預設方法

預設方法由 default 修飾符修飾,並像類中宣告的其他方法一樣包含方法體。比如,你可以像下面這樣在集合庫中定義一個名為Sized 的介面,在其中定義一個抽象方法 size ,以及一個預設方法 isEmpty :

public interface Sized {
    int size();
    default boolean isEmpty() {
        return size() == 0;
    }
}

這樣任何一個實現了 Sized 介面的類都會自動繼承 isEmpty 的實現。因此,向提供了預設實現的介面新增方法就不是原始碼相容的。 預設方法在Java 8的API中已經大量地使用了。本章已經介紹過我們前一章中大量使用的 Collection 介面的 stream 方法就是預設方法。 List 介面的 sort 方法也是預設方法。第3章介紹的很多函式式介面,比如 Predicate 、 Function 以及 Comparator 也引入了新的預設方法,比如 Predicate.and 或者 Function.andThen (記住,函式式介面只包含一個抽象方法,預設方法是種非抽象方法)。

3. 預設方法的使用模式

3.1 可選方法

類實現了介面,不過卻刻意地將一些方法的實現留白。我們以Iterator 介面為例來說。 Iterator 介面定義了 hasNext 、 next ,還定義了 remove 方法。Java 8之前,由於使用者通常不會使用該方法, remove 方法常被忽略。因此,實現 Interator 介面的類通常會為 remove 方法放置一個空的實現,這些都是些毫無用處的模板程式碼。採用預設方法之後,你可以為這種型別的方法提供一個預設的實現,這樣實體類就無需在自己的實現中顯式地提供一個空方法。比如,在Java 8中, Iterator 介面就為 remove 方法提供了一個預設實現,如下所示:

public interface Iterator<E> {
    ...
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
    ...
}

3.2 行為的多繼承

預設方法讓之前無法想象的事兒以一種優雅的方式得以實現,即行為的多繼承。這是一種讓類從多個來源重用程式碼的能力。

Java的類只能繼承單一的類,但是一個類可以實現多介面。要確認也很簡單,下面是Java API中對 ArrayList 類的定義:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
}

3.2.1 型別的多繼承

這個例子中 ArrayList 繼承了一個類,實現了六個介面。因此 ArrayList 實際是七個型別的直接子類,分別是: AbstractList 、 List 、 RandomAccess 、 Cloneable 、 Serializable 、Iterable 和 Collection 。所以,在某種程度上,我們早就有了型別的多繼承。 由於Java 8中介面方法可以包含實現,類可以從多個介面中繼承它們的行為(即實現的程式碼)。讓我們從一個例子入手,看看如何充分利用這種能力來為我們服務。保持介面的精緻性和正交效能幫助你在現有的程式碼基上最大程度地實現程式碼複用和行為組合。

3.2.2 利用正交方法的精簡介面

假設你需要為你正在建立的遊戲定義多個具有不同特質的形狀。有的形狀需要調整大小,但是不需要有旋轉的功能;有的需要能旋轉和移動,但是不需要調整大小。這種情況下,你怎麼設計才能儘可能地重用程式碼? 你可以定義一個單獨的 Rotatable 介面,並提供兩個抽象方法 setRotationAngle 和getRotationAngle ,如下所示:

public interface Rotatable {
    int getRotationAngle();
    void setRotationAngle(int angleInDegrees);
    default void rotateBy(int angleInDegrees) {
        setRotationAngle((getRotationAngle() + angleInDegrees) % 360);
    }
}

這種方式和模板設計模式有些相似,都是以其他方法需要實現的方法定義好框架演算法。 現在,實現了 Rotatable 的所有類都需要提供 setRotationAngle 和 getRotationAngle的實現,但與此同時它們也會天然地繼承 rotateBy 的預設實現。 類似地,你可以定義之前看到的兩個介面 Moveable 和 Resizable 。它們都包含了預設實現。下面是 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 組合介面

通過組合這些介面,你現在可以為你的遊戲建立不同的實體類。比如, Monster 可以移動、旋轉和縮放。

public class Monster implements Rotatable, Moveable, Resizable {
    ...
}

Monster 類會自動繼承 Rotatable 、 Moveable 和 Resizable 介面的預設方法。這個例子中,Monster 繼承了 rotateBy 、 moveHorizontally 、 moveVertically 和 setRelativeSize 的實現。 你現在可以直接呼叫不同的方法:

Monster m = new Monster();
m.rotateBy(180);
m.moveVertically(10);

像你的遊戲程式碼那樣使用預設實現來定義簡單的介面還有另一個好處。假設你需要修改moveVertically 的實現,讓它更高效地執行。你可以在 Moveable 介面內直接修改它的實現,所有實現該介面的類會自動繼承新的程式碼(這裡我們假設使用者並未定義自己的方法實現)。 通過前面的介紹,你已經瞭解了預設方法多種強大的使用模式。不過也可能還有一些疑惑:如果一個類同時實現了兩個介面,這兩個介面恰巧又提供了同樣的預設方法簽名,這時會發生什麼情況?類會選擇使用哪一個方法?這些問題,我們會在接下來的一節進行討論。

4. 解決衝突的規則

隨著預設方法在Java 8中引入,有可能出現一個類繼承了多個方法而它們使用的卻是同樣的函式簽名。這種情況下,類會選擇使用哪一個函式?接下來的例子主要用於說明容易出問題的場景,並不表示這些場景在實際開發過程中會經常發生。

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++語言中著名的菱形繼承問題有所瞭解,菱形繼承問題中一個類同時繼承了具有相同函式簽名的兩個方法。到底該選擇哪一個實現呢? Java 8也提供瞭解決這個問題的方案。請接著閱讀下面的內容。

4.1 解決問題的三條規則

如果一個類使用相同的函式簽名從多個地方(比如另一個類或介面)繼承了方法,通過三條規則可以進行判斷。

  1. 類中的方法優先順序最高。類或父類中宣告的方法的優先順序高於任何宣告為預設方法的優先順序。
  2. 如果無法依據第一條進行判斷,那麼子介面的優先順序更高:函式簽名相同時,優先選擇擁有最具體實現的預設方法的介面,即如果 B 繼承了 A ,那麼 B 就比 A 更加具體。
  3. 最後,如果還是無法判斷,繼承了多個介面的類必須通過顯式覆蓋和呼叫期望的方法,顯式地選擇使用哪一個預設方法的實現。

4.2 菱形繼承問題

瞭解即可

5. 小結

  1. Java 8中的介面可以通過預設方法和靜態方法提供方法的程式碼實現。
  2. 預設方法的開頭以關鍵字 default 修飾,方法體與常規的類方法相同。
  3. 向釋出的介面新增抽象方法不是原始碼相容的。
  4. 預設方法的出現能幫助庫的設計者以後向相容的方式演進API。
  5. 預設方法可以用於建立可選方法和行為的多繼承。
  6. 我們有辦法解決由於一個類從多個介面中繼承了擁有相同函式簽名的方法而導致的衝突。
  7. 類或者父類中宣告的方法的優先順序高於任何預設方法。如果前一條無法解決衝突,那就選擇同函式簽名的方法中實現得最具體的那個介面的方法。
  8. 兩個預設方法都同樣具體時,你需要在類中覆蓋該方法,顯式地選擇使用哪個介面中提供的預設方法。

資源獲取

  • 公眾號回覆 : Java8 即可獲取《Java 8 in Action》中英文版!

Tips

  • 歡迎收藏和轉發,感謝你的支援!(๑•̀ㅂ•́)و✧
  • 歡迎關注我的公眾號:莊裡程式猿,讀書筆記教程資源第一時間獲得!

相關推薦

Java 8 in Action》Chapter 9預設方法

傳統上,Java程式的介面是將相關方法按照約定組合到一起的方式。實現介面的類必須為介面中定義的每個方法提供一個實現,或者從父類中

採用Java 8中Lambda表示式和預設方法的模板方法模式

原文連結 作者:   Mohamed Sanaulla  譯者: 李璟([email protected]) 模板方法模式是“四人幫”(譯者注:Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides)所著《Design 

Java 8 In Action之引用特定型別的任意物件的例項方法

此種引用型別名稱原文為:reference to an instance method of an arbitrary object of a particular type 今天在和同學討論另外一個問題的時候(直接導致這個問題只有明天再解決了),突然爭論到能不能用類來呼叫

Java 8 in Action》Chapter 1為什麼要關心Java 8

自1998年 JDK 1.0(Java 1.0) 釋出以來,Java 已經受到了學生、專案經理和程式設計師等一大批活躍使用者的歡迎。這一語言極富活力,不斷被用在大大小小的專案裡。從 Java 1.1(1997年) 一直到 Java 7(2011年),Java 通過增加新功能,不斷得到良好的升級。Java 8

Java 8 in Action》Chapter 3Lambda表示式

1. Lambda簡介 可以把Lambda表示式理解為簡潔地表示可傳遞的匿名函式的一種方式:它沒有名稱,但它有引數列表、函式主體、返回型別,可能還有一個可以丟擲的異常列表。 匿名——我們說匿名,是因為它不像普通的方法那樣有一個明確的名稱:寫得少而想得多! 函式——我們說它是函式,是因為Lambda函式不像方

Java 8 in Action》Chapter 4引入流

1. 流簡介 流是Java API的新成員,它允許你以宣告性方式處理資料集合(通過查詢語句來表達,而不是臨時編寫一個實現)。就現在來說,你可以把它們看成遍歷資料集的高階迭代器。此外,流還可以透明地並行處理。讓我們來看一個例項返回低熱量(<400)的菜餚名稱: Java7版本: List<Dish&

Java 8 in Action》Chapter 6用流收集資料

1. 收集器簡介 collect() 接收一個型別為 Collector 的引數,這個引數決定了如何把流中的元素聚合到其它資料結構中。Collectors 類包含了大量常用收集器的工廠方法,toList() 和 toSet() 就是其中最常見的兩個,除了它們還有很多收集器,用來對資料進行對複雜的轉換。 指令式

Java 8 in Action》Chapter 7並行資料處理與效能

在Java 7之前,並行處理資料集合非常麻煩。第一,你得明確地把包含資料的資料結構分成若干子部分。第二,你要給每個子部分分配一個獨立的執行緒。第三,你需要在恰當的時候對它們進行同步來避免不希望出現的競爭條件,等待所有執行緒完成,最後把這些部分結果合併起來。Java 7引入了一個叫作分支/合併的框架,讓這些操

Java 8 in Action》Chapter 8重構、測試和除錯

我們會介紹幾種方法,幫助你重構程式碼,以適配使用Lambda表示式,讓你的程式碼具備更好的可讀性和靈活性。除此之外,我們還會討論目前比較流行的幾種面向物件的設計模式, 包括策略模式、模板方法模式、觀察者模式、責任鏈模式,以及工廠模式,在結合Lambda表示式之後變得更簡潔的情況。最後,我們會介紹如何測試和除

Java 8 in Action》Chapter 10用Optional取代null

1965年,英國一位名為Tony Hoare的電腦科學家在設計ALGOL W語言時提出了null引用的想法。ALGOL W是第一批在堆上分配記錄的型別語言之一。Hoare選擇null引用這種方式,“只是因為這種方法實現起來非常容易”。雖然他的設計初衷就是要“通過編譯器的自動檢測機制,確保所有使用引用的地方都

Java 8 in Action》Chapter 11CompletableFuture組合式非同步程式設計

某個網站的資料來自Facebook、Twitter和Google,這就需要網站與網際網路上的多個Web服務通訊。可是,你並不希望因為等待某些服務的響應,阻塞應用程式的執行,浪費數十億寶貴的CPU時鐘週期。比如,不要因為等待Facebook的資料,暫停對來自Twitter的資料處理。 第7章中介紹的分支/合

How to Install Oracle JAVA 8 on Debian 9 / Debian 8

tar int rac oracle linu https min html .com https://www.itzgeek.com/how-tos/linux/debian/how-to-install-oracle-java-8-on-debian-9-ubuntu-

C#設計模式之9模板方法

like not 存在 als col wan 結構 允許 封裝 模板方法 模板方法是一個方法,定義了算法的步驟,並允許子類為一個或多個步驟提供實現。 本例中用沖泡咖啡和茶的例子來說明: 上圖說明了沖泡咖啡和茶的步驟,可以看出沖泡咖啡和茶的步驟差不多,很相似,先來看看沒有

9魔術方法

魔術方法的定義 就是那些在某些特定的時刻,會自動執行的方法,統稱為魔術方法。 他們最大的特點是以:__雙下劃線開頭。 例如建構函式,解構函式,克隆函式,__get函式,__set函式,都是魔術方法。 在PHP中系統自帶的函式和方法就有四千多個,記得住嗎???

NETWORK筆記9預設路由與浮動路由

預設路由:一種特殊的靜態路由,只有從路由表中找不到任何明確匹配的路由條目時,才會使用預設路由。(當訪問Internet時,一些網路出口只有一個,此時沒有必要配置) 浮動路由:配置一個管理距離更大的靜態路由,作為應急出發的備份路徑,在主路由有效的情況下,浮動路由不會出現在路由表中。 實驗名稱:華為路

Java JDK原始碼解析之native方法

初次看見native關鍵字是自己在看Scanner類原始碼中傳遞System.in引數實現列印,之後轉到System觀看原始碼時看見native關鍵字,關於native關鍵字筆者表示,是Java與C語言的通訊介面,因為Java語言沒有操作底層的條件,所以Java

Java 8實戰(Java 8 in action)學習總結(三)

Streams API可以表達複雜的資料處理查詢。常用的流操作如下表: 你可以使用filter、distinct、skip和limit對流做篩選和切片。 你可以使用map和flatMap提取或轉換流中的元素。 你可以使用findFirst和findAny方法查詢流中的元素。你可以allMatch、none

Ask HN: Have you moved beyond Java 8 in production?

Have you moved to Java 9 , 10 or 11 in production ? Yes / No ? How big is your source code ? Is it a legacy code ? Does it have automated tests ? How much

Java 8特性探究(1)通往lambda之路

函式式介面 函式式介面(functional interface 也叫功能性介面,其實是同一個東西)。簡單來說,函式式介面是隻包含一個方法的介面。比如Java標準庫中的java.lang.Runnable和 java.util.Comparator都是典型的函式式介面。java 8提供 @F

Java多執行緒(9)Lock

在Java多執行緒中 使用ReentrantLock類也能達到同步的效果使用Condition實現等待/通知在使用notify()/notifyAll()進行執行緒通知 被通知的執行緒卻是由JVM隨機選擇的 但是使用ReentrantLock結合Condition類可以實現選