默認(rèn)方法,真香,開(kāi)動(dòng)!接口?我要升級(jí)?。?/font>

作者:xcbeyond
瘋狂源自夢(mèng)想,技術(shù)成就輝煌!微信公眾號(hào):《程序猿技術(shù)大咖》號(hào)主,專(zhuān)注后端開(kāi)發(fā)多年,擁有豐富的研發(fā)經(jīng)驗(yàn),樂(lè)于技術(shù)輸出、分享,現(xiàn)階段從事微服務(wù)架構(gòu)項(xiàng)目的研發(fā)工作,涉及架構(gòu)設(shè)計(jì)、技術(shù)選型、業(yè)務(wù)研發(fā)等工作。對(duì)于Java、微服務(wù)、數(shù)據(jù)庫(kù)、Docker有深入了解,并有大量的調(diào)優(yōu)經(jīng)驗(yàn)。

一、接口可以升級(jí)嗎?
在《Java編程思想·第4版》一書(shū)中提到:

interface這個(gè)關(guān)鍵字產(chǎn)生一個(gè)完全抽象的類(lèi),它根本就沒(méi)有提供任何具體的實(shí)現(xiàn)。它允許創(chuàng)建者確定方法名、參數(shù)列表和返回類(lèi)型,但是沒(méi)有任何方法體。接口只提供了形式,而未提供任何具體實(shí)現(xiàn)。

我們?cè)贘ava入門(mén)學(xué)習(xí)時(shí),也知道接口只提供方法的聲明,具體實(shí)現(xiàn)必須在對(duì)應(yīng)的實(shí)現(xiàn)類(lèi)中實(shí)現(xiàn)。實(shí)現(xiàn)接口的類(lèi)必須為接口中定義的每個(gè)方法提供一個(gè)實(shí)現(xiàn),否則就連編譯都無(wú)法通過(guò)。

隨著之前定義的接口,被廣泛實(shí)現(xiàn)使用,一旦需要升級(jí)接口,在接口中新增方法,將不得不通知所有實(shí)現(xiàn)類(lèi)來(lái)實(shí)現(xiàn)該新增方法。想象一下,你作為一個(gè)接口實(shí)現(xiàn)者,愿意莫名其妙的來(lái)實(shí)現(xiàn)一些自己壓根就沒(méi)用的方法么?這簡(jiǎn)直就會(huì)讓人抓狂、瘋掉!難道就沒(méi)有解決辦法了么?

且慢,其實(shí)你不必抓狂。JDK1.8的出現(xiàn),解決了這些問(wèn)題,從中引入了一種新的機(jī)制。JDK1.8中的接口支持在聲明方法的同時(shí)提供實(shí)現(xiàn),通過(guò)兩種方式可以完成這種操作:

JDK1.8允許在接口內(nèi)聲明靜態(tài)方法。
JDK1.8引入了一個(gè)新功能,叫默認(rèn)方法,通過(guò)默認(rèn)方法可以指定接口方法的默認(rèn)實(shí)現(xiàn)。
換句話(huà)說(shuō),接口中能夠提供方法的具體實(shí)現(xiàn)。因此,實(shí)現(xiàn)接口的類(lèi)如果不顯示提供該方法的具體實(shí)現(xiàn),就會(huì)自動(dòng)繼承默認(rèn)的實(shí)現(xiàn)。

默認(rèn)方法的出現(xiàn)可以使你輕松、平滑地進(jìn)行接口的優(yōu)化、升級(jí)。

實(shí)際上,從你使用JDK1.8開(kāi)始就已經(jīng)使用了多個(gè)默認(rèn)方法,比如List接口中的sort方法、以及Collection接口中的Stream。

public interface List<E> extends Collection<E> {
    ……
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }
    ……
}


可以看到,這個(gè)新增的sort方法有方法體,由default修飾符修飾,這就是接口的默認(rèn)方法。

太開(kāi)心了,在JDK1.8之后,接口居然可以這么輕松升級(jí)啦。

二、接口如何升級(jí)呢?
假如在一個(gè)繪圖庫(kù)中,存在一個(gè)接口Resizable,它定義了一個(gè)簡(jiǎn)單的可縮放形狀必須支持的很多方法, 比如:setHeight、setWidth、getHeight、getWidth等。

此外,你還提供了幾個(gè)額外的實(shí)現(xiàn),如正方形、長(zhǎng)方形。由于你的庫(kù)非常流行,被廣為流傳,你的一些用戶(hù)使用Resizable接口,創(chuàng)建了他們自己感興趣的實(shí)現(xiàn),比如橢圓。

接口Resizable如下:

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

你的一位鐵桿用戶(hù)根據(jù)自身的需求實(shí)現(xiàn)了Resizable接口,創(chuàng)建了Ellipse類(lèi):

public class Ellipse implements Resizable { 
     … 
}

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

庫(kù)上線(xiàn)使用幾個(gè)月之后,你收到很多請(qǐng)求,要求你更新Resizable的實(shí)現(xiàn),讓Square、Rectangle以及其他的形狀都能支持setRelativeSize方法。為了滿(mǎn)足這些新的需求,你不得不來(lái)升級(jí)你接口。

如果直接在接口Resizable中,新增setRelativeSize方法:

public interface Resizable { 
     int getWidth(); 
     int getHeight(); 
     void setWidth(int width); 
     void setHeight(int height);
     void setAbsoluteSize(int width, int height);
     // 新增方法
     void setRelativeSize(int wFactor, int hFactor);
}

將會(huì)導(dǎo)致一系列的問(wèn)題,不得不要求接口的實(shí)現(xiàn)者(庫(kù)用戶(hù))強(qiáng)制來(lái)添加setAbsoluteSize方法的實(shí)現(xiàn),這肯定是不太現(xiàn)實(shí)的升級(jí)方法。

這時(shí),我們想到了默認(rèn)方法,便可以解決上面這種問(wèn)題。不但滿(mǎn)足了新用戶(hù)的需求,而且也兼容了老用戶(hù)。






public interface Resizable { 
     int getWidth(); 
     int getHeight(); 
     void setWidth(int width); 
     void setHeight(int height);
     void setAbsoluteSize(int width, int height);
     // 新增方法setRelativeSize,即:采用默認(rèn)方法
     default void setRelativeSize(int wFactor, int hFactor){ 
         setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor); 
    }
}

如果用戶(hù)對(duì)方法setRelativeSize的功能有新的想法,自己完全也可以重寫(xiě)該方法。

完美,至此升級(jí)完成!

三、默認(rèn)方法的妙用
默認(rèn)方法的引入就是為了以兼容的方式解決像 Java API這樣的類(lèi)庫(kù)的演進(jìn)問(wèn)題的,如下圖所示:
 

簡(jiǎn)而言之,升級(jí)接口中的方法是諸多問(wèn)題的罪惡之源;一旦接口發(fā)生變化,實(shí)現(xiàn)這些接口的類(lèi)往往也需要更新,提供新添方法的實(shí)現(xiàn)才能適配接口的變化。

現(xiàn)在你已經(jīng)了解了默認(rèn)方法是怎樣以兼容的方式來(lái)升級(jí)接口。除了上面的使用場(chǎng)景,還有其他場(chǎng)景也能利用這個(gè)新特性嗎?當(dāng)然有,你可以創(chuàng)建自己的接口,并為其提供默認(rèn)方法。接下來(lái),就介紹使用默認(rèn)方法的兩種場(chǎng)景:

可選方法

多繼承

1. 可選方法
你很可能也碰到過(guò)這種情況,類(lèi)實(shí)現(xiàn)了接口,不過(guò)卻刻意地將一些方法的實(shí)現(xiàn)留白。我們以Iterator接口為例來(lái)說(shuō)。Iterator接口定義了hasNext、next,還定義了remove方法。

Java 8之前,由于用戶(hù)通常不會(huì)使用該方法,remove方法常被忽略。因此,實(shí)現(xiàn)Interator接口的類(lèi)通常會(huì)為remove方法放置一個(gè)空的實(shí)現(xiàn),這些都是些毫無(wú)用處的模板代碼。

采用默認(rèn)方法之后,你可以為這種類(lèi)型的方法提供一個(gè)默認(rèn)的實(shí)現(xiàn),這樣實(shí)體類(lèi)就無(wú)需在自己的實(shí)現(xiàn)中顯式地提供一個(gè)空方法。比如,在Java 8中,Iterator接口就為remove方法提供了一個(gè)默認(rèn)實(shí)現(xiàn),如下所示:

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

通過(guò)這種方式,你可以減少無(wú)效的模板代碼。實(shí)現(xiàn)Iterator接口的每一個(gè)類(lèi)都不需要再聲明一個(gè)空的remove方法了,因?yàn)樗F(xiàn)在已經(jīng)有一個(gè)默認(rèn)的實(shí)現(xiàn)。

2. 多繼承
默認(rèn)方法讓之前無(wú)法想象的事兒以一種優(yōu)雅的方式得以實(shí)現(xiàn),即多繼承。這是一種讓類(lèi)從多個(gè)來(lái)源重用代碼的能力,如圖下圖:
 

看到這里,不要誤認(rèn)為Java是支持多繼承的哦。切記:“Java的類(lèi)只能繼承單一的類(lèi),但是一個(gè)類(lèi)可以實(shí)現(xiàn)多接口?!?/p>

比如,我們?cè)O(shè)計(jì)一款游戲,需要定義多個(gè)具有不同特質(zhì)的性質(zhì)。有的形狀需要調(diào)整大小,但是不需要有旋轉(zhuǎn)的功能;有的需要能旋轉(zhuǎn)和移動(dòng),但是不需要調(diào)整大小。我們可以如何設(shè)計(jì)呢?這里就可以用到默認(rèn)方法了。

定義一個(gè)可旋轉(zhuǎn)接口,并提供旋轉(zhuǎn)的默認(rèn)方法:

public interface Rotatable {     
    void setRotationAngle(int angleInDegrees);     
    int getRotationAngle();
    // 旋轉(zhuǎn)方法
    default void rotateBy(int angleInDegrees){           
        setRotationAngle((getRotationAngle () + angle) % 360);     
    } 
}

定義一個(gè)可移動(dòng)接口,并提供移動(dòng)的默認(rèn)方法:

public interface Moveable {     
    int getX();     
    int getY();     
    void setX(int x);     
    void setY(int y); 
    // 移動(dòng)方法
    default void moveHorizontally(int distance){        
         setX(getX() + distance);    
    } 
    default void moveVertically(int distance){         
        setY(getY() + distance);     
    } 
 } 

定義一個(gè)可改變大小接口,并提供改變大小的默認(rèn)方法:

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)建不同的實(shí)體類(lèi),比如Monster可以移動(dòng)、 旋轉(zhuǎn)和縮放。

public class Monster implements Rotatable, Moveable, Resizable { 
    //... 只需要給出所有抽象方法的實(shí)現(xiàn),不需要重復(fù)實(shí)現(xiàn)默認(rèn)方法
 } 

Monster繼承了rotateBy、moveHorizontally、moveVertically和setRelativeSize的實(shí)現(xiàn)。我們也可以直接調(diào)用不同的方法:

Monster m = new Monster(); 
//調(diào)用由Rotatable中繼承而來(lái)的rotateBy 
m.rotateBy(180);  
//調(diào)用由Moveable中繼承而來(lái)的moveVertically 
m.moveVertically(10); 

假設(shè)你現(xiàn)在需要聲明另一個(gè)類(lèi),它要能移動(dòng)和旋轉(zhuǎn),但是不能縮放,比如說(shuō)Sun。這時(shí)也無(wú)需復(fù)制粘貼代碼,你可以像下面這樣復(fù)用Moveable和Rotatable接口的默認(rèn)實(shí)現(xiàn)。

public class Sun implements Moveable, Rotatable {
    …  


四、總結(jié)
通過(guò)本篇文章,認(rèn)識(shí)了Java8新引入的默認(rèn)方法,也通過(guò)實(shí)例看到了這種默認(rèn)方法帶來(lái)的靈活性,更能夠輕松升級(jí)接口中的方法,所以此后若在項(xiàng)目中,需要對(duì)接口進(jìn)行改動(dòng),也可以想想是否可以使用默認(rèn)方法。