日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

《Java8实战》笔记(09):默认方法

發(fā)布時間:2023/12/13 java 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 《Java8实战》笔记(09):默认方法 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

默認方法

本文的源碼

實現(xiàn)接口的類必須為接口中定義的每個方法提供一個實現(xiàn),或者從父類中繼承它的實現(xiàn)。但是,一旦類庫的設(shè)計者需要更新接口,向其中加入新的方法,這種方式就會出現(xiàn)問題。現(xiàn)實情況是,現(xiàn)存的實體類往往不在接口設(shè)計者的控制范圍之內(nèi),這些實體類為了適配新的接口約定也需要進行修改。由于Java 8的API在現(xiàn)存的接口上引入了非常多的新方法,這種變化帶來的問題也愈加嚴重,一個例子就是像Guava和Apache Commons這樣的框架現(xiàn)在都需要修改實現(xiàn)了List接口的所有類,為其添加sort方法的實現(xiàn)。

Java 8為了解決這一問題引入了一種新的機制。Java 8中的接口現(xiàn)在支持在聲明方法的同時提供實現(xiàn)!通過兩種方式可以完成這種操作。

  • Java 8允許在接口內(nèi)聲明靜態(tài)方法。
  • Java 8引入了一個新功能,叫默認方法,通過默認方法你可以指定接口方法的默認實現(xiàn)。
  • 換句話說,接口能提供方法的具體實現(xiàn)。因此,實現(xiàn)接口的類如果不顯式地提供該方法的具體實現(xiàn),就會自動繼承默認的實現(xiàn)。這種機制可以使你平滑地進行接口的優(yōu)化和演進。實際上,到目前為止你已經(jīng)使用了多個默認方法。


    兩個例子

    List接口中的sort方法是Java 8中全新的方法,它的定義如下:

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

    Collection中的stream方法的定義如下

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

    默認方法的引入就是為了以兼容的方式解決像Java API這樣的類庫的演進問題的

    不斷演進的API

    為了理解為什么一旦API發(fā)布之后,它的演進就變得非常困難,

    假設(shè)你是一個流行Java繪圖庫的設(shè)計者

    你的庫中包含了一個Resizable接口,它定義了一個簡單的可縮放形狀必須支持的很多方法, 比如:setHeight、setWidth、getHeight、getWidth以及setAbsoluteSize。此外,你還提供了幾個額外的實現(xiàn)(out-of-box implementation),如正方形、長方形。由于你的庫非常流行,你的一些用戶使用Resizable接口創(chuàng)建了他們自己感興趣的實現(xiàn),比如橢圓。

    發(fā)布API幾個月之后,你突然意識到Resizable接口遺漏了一些功能。比如,如果接口提供一個setRelativeSize方法,可以接受參數(shù)實現(xiàn)對形狀的大小進行調(diào)整,那么接口的易用性會更好。

    你會說這看起來很容易啊:為Resizable接口添加setRelativeSize方法,再更新Square和Rectangle的實現(xiàn)就好了。不過,事情并非如此簡單!你要考慮已經(jīng)使用了你接口的用戶,他們已經(jīng)按照自身的需求實現(xiàn)了Resizable接口,他們該如何應對這樣的變更呢

    非常不幸,你無法訪問,也無法改動他們實現(xiàn)了Resizable接口的類。這也是Java庫的設(shè)計者需要改進Java API時面對的問題。

    初始版本的API

    public interface Resizable extends Drawable{int getWidth();int getHeight();void setWidth(int width);void setHeight(int height);void setAbsoluteSize(int width, int height); }

    用戶實現(xiàn)

    用戶根據(jù)自身的需求實現(xiàn)了Resizable接口,創(chuàng)建了Ellipse類:

    public class Ellipse implements Resizable {}

    他實現(xiàn)了一個處理各種Resizable形狀(包括Ellipse)的游戲:

    public class Game{public static void main(String...args){List<Resizable> resizableShapes =Arrays.asList(new Square(), new Rectangle(), new Ellipse());Utils.paint(resizableShapes);} } public class Utils{public static void paint(List<Resizable> l){l.forEach(r -> {r.setAbsoluteSize(42, 42);r.draw();});} }

    第二版API

    庫上線使用幾個月之后,你收到很多請求,要求你更新Resizable的實現(xiàn),讓Square、Rectangle以及其他的形狀都能支持setRelativeSize方法。為了滿足這些新的需求,你發(fā)布了第二版API

    public interface Resizable {//...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方法中調(diào)用setRelativeSize方法,因為paint方法接受一個Resizable對象列表作為參數(shù)。如果傳遞的是一個Ellipse對象,程序就會拋出一個運行時錯誤,因為它并未實現(xiàn)setRelativeSize方法:

    Exception in thread "main" java.lang.AbstractMethodError:com.lun.c09.Ellipse.setRelativeSize(II)V

    其次,如果用戶試圖重新編譯整個應用(包括Ellipse類),他會遭遇下面的編譯錯誤:

    com/lun/c09/Ellipse.java:6: error: Ellipse is not abstract and does not override abstract method setRelativeSize(int,int) in Resizable

    最后,更新已發(fā)布API會導致后向兼容性問題。這就是為什么對現(xiàn)存API的演進,比如官方發(fā)布的Java Collection API,會給用戶帶來麻煩。當然,還有其他方式能夠?qū)崿F(xiàn)對API的改進,但是都不是明智的選擇。比如,你可以為你的API創(chuàng)建不同的發(fā)布版本,同時維護老版本和新版本,但這是非常費時費力的,原因如下。

  • 這增加了你作為類庫的設(shè)計者維護類庫的復雜度。
  • 類庫的用戶不得不同時使用一套代碼的兩個版本,而這會增大內(nèi)存的消耗,延長程序的載入時間,因為這種方式下項目使用的類文件數(shù)量更多了。
  • 不同類型的兼容性:二進制、源代碼和函數(shù)行為

    變更對Java程序的影響大體可以分成三種類型的兼容性,分別是:二進制級的兼容、源代碼級的兼容,以及函數(shù)行為的兼容。

  • 二進制級的兼容性表示現(xiàn)有的二進制執(zhí)行文件能無縫持續(xù)鏈接(包括驗證、準備和解析)和運行。比如,為接口添加一個方法就是二進制級的兼容,這種方式下,如果新添加的方法不被調(diào)用,接口已經(jīng)實現(xiàn)的方法可以繼續(xù)運行,不會出現(xiàn)錯誤。

  • 源代碼級的兼容性表示引入變化之后,現(xiàn)有的程序依然能成功編譯通過。比如,向接口添加新的方法就不是源碼級的兼容,因為遺留代碼并沒有實現(xiàn)新引入的方法,所以它們無法順利通過編譯。

  • 函數(shù)行為的兼容性表示變更發(fā)生之后,程序接受同樣的輸入能得到同樣的結(jié)果。比如,為接口添加新的方法就是函數(shù)行為兼容的,因為新添加的方法在程序中并未被調(diào)用(抑或該接口在實現(xiàn)中被覆蓋了)。

  • 概述默認方法

    默認方法由default修飾符修飾,并像類中聲明的其他方法一樣包含方法體。比如,你可以像下面這樣在集合庫中定義一個名為Sized的接口,在其中定義一個抽象方法size,以及一個默認方法isEmpty:

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

    這樣任何一個實現(xiàn)了Sized接口的類都會自動繼承isEmpty的實現(xiàn)。因此,向提供了默認實現(xiàn)的接口添加方法就 不是 源碼兼容的。

    現(xiàn)在,我們回顧一下最初的例子,那個Java畫圖類庫和你的游戲程序。具體來說,為了以兼容的方式改進這個庫(即使用該庫的用戶不需要修改他們實現(xiàn)了Resizable的類),可以使用默認方法,提供setRelativeSize的默認實現(xiàn):

    default void setRelativeSize(int wFactor, int hFactor){setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor); }

    默認方法在Java 8的API中已經(jīng)大量地使用了

    Collection接口的stream方法就是默認方法。List接口的sort方法也是默認方法。

    函數(shù)式接口,比如Predicate、Function以及Comparator也引入了新的默認方法,比如Predicate.and或者Function.andThen

    Java 8中的抽象類和抽象接口

    那么抽象類和抽象接口之間的區(qū)別是什么呢?它們不都能包含抽象方法和包含方法體的實現(xiàn)嗎?

    • 一個類只能繼承一個抽象類,但是一個類可以實現(xiàn)多個接口。
    • 一個抽象類可以通過實例變量(字段)保存一個通用狀態(tài),而接口是不能有實例變量的。

    使用默認實現(xiàn)的例子——removeIf

    這個測驗里,假設(shè)你是Java語言和API的一個負責人。你收到了關(guān)于removeIf方法的很多請求,希望能為ArrayList、TreeSet、LinkedList以及其他集合類型添加removeIf方法。removeIf方法的功能是刪除滿足給定謂詞的所有元素。你的任務是找到添加這個新方法、優(yōu)化Collection API的最佳途徑。

    改進Collection API破壞性最大的方式是什么?你可以把removeIf的實現(xiàn)直接復制到Collection API的每個實體類中,但這種做法實際是在對Java界的犯罪。還有其他的方式嗎?

    所有的Collection類都實現(xiàn)了一個名為java.util.Collection的接口。太好了,那么我們可以在這里添加一個方法?是的!你只需要牢記,默認方法是一種以源碼兼容方式向接口內(nèi)添加實現(xiàn)的方法。這樣實現(xiàn)Collction的所有類(包括并不隸屬Collection API的用戶擴展類)都能使用removeIf的默認實現(xiàn)。removeIf的代碼實現(xiàn)如下(它實際就是Java 8 Collection API的實現(xiàn))。

    default boolean removeIf(Predicate<? super E> filter) {boolean removed = false;Iterator<E> each = iterator();while(each.hasNext()) {if(filter.test(each.next())) {each.remove();removed = true;}}return removed; }

    默認方法的使用模式

    可選方法

    類實現(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),如下所示:

    interface Iterator<T> {boolean hasNext();T next();default void remove() {throw new UnsupportedOperationException();} }

    通過這種方式,你可以減少無效的模板代碼。實現(xiàn)Iterator接口的每一個類都不需要再聲明一個空的remove方法了,因為它現(xiàn)在已經(jīng)有一個默認的實現(xiàn)。

    行為的多繼承

    默認方法讓之前無法想象的事兒以一種優(yōu)雅的方式得以實現(xiàn),即行為的多繼承。這是一種讓類從多個來源重用代碼的能力

    Java的類只能繼承單一的類,但是一個類可以實現(xiàn)多接口。下面是Java API中對ArrayList類的定義:

    public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable,Serializable, Iterable<E>, Collection<E> { }

    類型的多繼承

    這個例子中ArrayList繼承了一個類,實現(xiàn)了六個接口。因此ArrayList實際是七個類型的直接子類,分別是:AbstractList、List、RandomAccess、Cloneable、Serializable、Iterable和Collection。所以,在某種程度上,我們早就有了類型的多繼承。

    由于Java 8中接口方法可以包含實現(xiàn),類可以從多個接口中繼承它們的行為(即實現(xiàn)的代碼)。

    保持接口的精致性和正交性能幫助你在現(xiàn)有的代碼基上最大程度地實現(xiàn)代碼復用和行為組合。

    利用正交方法的精簡接口

    假設(shè)你需要為你正在創(chuàng)建的游戲定義多個具有不同特質(zhì)的形狀。有的形狀需要調(diào)整大小,但是不需要有旋轉(zhuǎn)的功能;有的需要能旋轉(zhuǎn)和移動,但是不需要調(diào)整大小。

    你可以定義一個單獨的Rotatable接口,并提供兩個抽象方法setRotationAngle和getRotationAngle,如下所示

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

    這種方式和模板設(shè)計模式有些相似,都是以其他方法需要實現(xiàn)的方法定義好框架算法。

    現(xiàn)在,實現(xiàn)了Rotatable的所有類都需要提供setRotationAngle和getRotationAngle的實現(xiàn),但與此同時它們也會天然地繼承rotateBy的默認實現(xiàn)。

    類似地,你可以定義之前看到的兩個接口Moveable和Resizable。它們都包含了默認實現(xiàn)。下面是Moveable的代碼:

    public interface Moveable {int getX();int getY();void setX(int x);void setY(int y);default void moveHorizontally(int distance){setX(getX() + distance);}default void moveVertically(int distance){setY(getY() + distance);} }public interface Resizable {int getWidth();int getHeight();void setWidth(int width);void setHeight(int height);void setAbsoluteSize(int width, int height);default void setRelativeSize(int wFactor, int hFactor){setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor);} }

    組合接口

    通過組合這些接口,你現(xiàn)在可以為你的游戲創(chuàng)建不同的實體類。

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

    像你的游戲代碼那樣使用默認實現(xiàn)來定義簡單的接口還有另一個好處。假設(shè)你需要修改moveVertically的實現(xiàn),讓它更高效地運行。你可以在Moveable接口內(nèi)直接修改它的實現(xiàn),所有實現(xiàn)該接口的類會自動繼承新的代碼

    繼承不應該成為你一談到代碼復用就試圖倚靠的萬金油

    比如,從一個擁有100個方法及字段的類進行繼承就不是個好主意,因為這其實會引入不必要的復雜性。你完全可以使用代理有效地規(guī)避這種窘境,即創(chuàng)建一個方法通過該類的成員變量直接調(diào)用該類的方法。

    這就是為什么有的時候我們發(fā)現(xiàn)有些類被刻意地聲明為final類型:聲明為final的類不能被其他的類繼承,避免發(fā)生這樣的反模式,防止核心代碼的功能被污染。注意,有的時候聲明為final的類都會有其不同的原因,比如,String類被聲明為final,因為我們不希望有人對這樣的核心功能產(chǎn)生干擾。

    這種思想同樣也適用于使用默認方法的接口。通過精簡的接口,你能獲得最有效的組合,因為你可以只選擇你需要的實現(xiàn)。

    解決沖突的規(guī)則

    我們知道Java語言中一個類只能繼承一個父類,但是一個類可以實現(xiàn)多個接口。隨著默認方法在Java 8中引入,有可能出現(xiàn)一個類繼承了多個方法而它們使用的卻是同樣的函數(shù)簽名

    這種情況下,類會選擇使用哪一個函數(shù)?在實際情況中,像這樣的沖突可能極少發(fā)生,但是一旦發(fā)生這樣的狀況,必須要有一套規(guī)則來確定按照什么樣的約定處理這些沖突。

    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 B, A {public static void main(String... args) {new C().hello();} }

    解決問題的三條規(guī)則

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

  • 類中的方法優(yōu)先級最高。類或父類中聲明的方法的優(yōu)先級高于任何聲明為默認方法的優(yōu)先級。

  • 如果無法依據(jù)第一條進行判斷,那么子接口的優(yōu)先級更高:函數(shù)簽名相同時,優(yōu)先選擇擁有最具體實現(xiàn)的默認方法的接口,即如果B繼承了A,那么B就比A更加具體。

  • 最后,如果還是無法判斷,繼承了多個接口的類必須通過顯式覆蓋和調(diào)用期望的方法

  • 選擇提供了最具體實現(xiàn)的默認方法的接口

    例子中C類同時實現(xiàn)了B接口和A接口,而這兩個接口恰巧又都定義了名為hello的默認方法。另外,B繼承自A。

    編譯器會使用聲明的哪一個hello方法呢?按照規(guī)則(2),應該選擇的是提供了最具體實現(xiàn)的默認方法的接口。由于B比A更具體,所以應該選擇B的hello方法。所以,程序會打印輸出“Hello from B”。


    如果C像下面這樣繼承自D

    依據(jù)規(guī)則(1),類中聲明的方法具有更高的優(yōu)先級。D并未覆蓋hello方法,可是它實現(xiàn)了接口A。所以它就擁有了接口A的默認方法。

    規(guī)則(2)說如果類或者父類沒有對應的方法,那么就應該選擇提供了最具體實現(xiàn)的接口中的方法。因此,編譯器會在接口A和接口B的hello方法之間做選擇。由于B更加具體,所以程序會再次打印輸出“Hello from B”。


    在這個測驗中繼續(xù)復用之前的例子,唯一的不同在于D現(xiàn)在顯式地覆蓋了從A接口中繼承的hello方法。輸出會是什么呢?

    public class D implements A{void hello(){System.out.println("Hello from D");} }public class C extends D implements B, A {public static void main(String... args) {new C().hello();} }

    由于依據(jù)規(guī)則(1),父類中聲明的方法具有更高的優(yōu)先級,所以程序會打印輸出“Hello from D”。

    注意,D的聲明如下:

    public abstract class D implements A {public abstract void hello(); }

    這樣的結(jié)果是,雖然在結(jié)構(gòu)上,其他的地方已經(jīng)聲明了默認方法的實現(xiàn),C還是必須提供自己的hello方法

    沖突及如何顯式地消除歧義

    假設(shè)B不再繼承A

    public interface A {void hello() {System.out.println("Hello from A");} } public interface B {void hello() {System.out.println("Hello from B");} } public class C implements B, A { }

    這時規(guī)則(2)就無法進行判斷了,因為從編譯器的角度看沒有哪一個接口的實現(xiàn)更加具體,兩個都差不多。A接口和B接口的hello方法都是有效的選項。所以,Java編譯器這時就會拋出一個編譯錯誤,因為它無法判斷哪一個方法更合適:“Error: class C inherits unrelated defaults for hello()from types B and A.”

    沖突的解決

    解決這種兩個可能的有效方法之間的沖突,沒有太多方案;你只能顯式地決定你希望在C中使用哪一個方法。

    為了達到這個目的,你可以覆蓋類C中的hello方法,在它的方法體內(nèi)顯式地調(diào)用你希望調(diào)用的方法。

    Java 8中引入了一種新的語法X.super.m(…),其中X是你希望調(diào)用的m方法所在的父接口。舉例來說,如果你希望C使用來自于B的默認方法,它的調(diào)用方式看起來就如下所示:

    public class C implements B, A {void hello(){B.super.hello();} }

    假設(shè)接口A和B的聲明如下所示:

    public interface A{default Number getNumber(){return 10;} } public interface B{default Integer getNumber(){return 42;} }

    類C的聲明如下:

    public class C implements B, A {public static void main(String... args) {System.out.println(new C().getNumber());} }

    輸出什么呢?

    答案:類C無法判斷A或者B到底哪一個更加具體。這就是類C無法通過編譯的原因。

    菱形繼承問題

    public interface A{default void hello(){System.out.println("Hello from A");} }public interface B extends A { }public interface C extends A { }public class D implements B, C {public static void main(String... args) {new D().hello();} }

    類D中的默認方法到底繼承自什么地方 ——源自B的默認方法,還是源自C的默認方法?實際上只有一個方法聲明可以選擇。只有A聲明了一個默認方法。由于這個接口是D的父接口,代碼會打印輸出“Hello from A”。


    如果B中也提供了一個默認的hello方法,并且函數(shù)簽名跟A中的方法也完全一致,

    根據(jù)規(guī)則(2),編譯器會選擇提供了更具體實現(xiàn)的接口中的方法。由于B比A更加具體,所以編譯器會選擇B中聲明的默認方法。

    如果B和C都使用相同的函數(shù)簽名聲明了hello方法,就會出現(xiàn)沖突,你需要顯式地指定使用哪個方法。


    如果你在C接口中添加一個抽象的hello方法(這次添加的不是一個默認方法),會發(fā)生什么情況呢?

    public interface C extends A {void hello(); }

    這個新添加到C接口中的抽象方法hello比由接口A繼承而來的hello方法擁有更高的優(yōu)先級,因為C接口更加具體。因此,類D現(xiàn)在需要為hello顯式地添加實現(xiàn),否則該程序無法通過編譯。

    小結(jié)

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

    總結(jié)

    以上是生活随笔為你收集整理的《Java8实战》笔记(09):默认方法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。