默認(rèn)方法,真香,開動!接口?我要升級??!
作者:xcbeyond
瘋狂源自夢想,技術(shù)成就輝煌!微信公眾號:《程序猿技術(shù)大咖》號主,專注后端開發(fā)多年,擁有豐富的研發(fā)經(jīng)驗,樂于技術(shù)輸出、分享,現(xiàn)階段從事微服務(wù)架構(gòu)項目的研發(fā)工作,涉及架構(gòu)設(shè)計、技術(shù)選型、業(yè)務(wù)研發(fā)等工作。對于Java、微服務(wù)、數(shù)據(jù)庫、Docker有深入了解,并有大量的調(diào)優(yōu)經(jīng)驗。
一、接口可以升級嗎?
在《Java編程思想·第4版》一書中提到:
interface這個關(guān)鍵字產(chǎn)生一個完全抽象的類,它根本就沒有提供任何具體的實現(xiàn)。它允許創(chuàng)建者確定方法名、參數(shù)列表和返回類型,但是沒有任何方法體。接口只提供了形式,而未提供任何具體實現(xiàn)。
我們在Java入門學(xué)習(xí)時,也知道接口只提供方法的聲明,具體實現(xiàn)必須在對應(yīng)的實現(xiàn)類中實現(xiàn)。實現(xiàn)接口的類必須為接口中定義的每個方法提供一個實現(xiàn),否則就連編譯都無法通過。
隨著之前定義的接口,被廣泛實現(xiàn)使用,一旦需要升級接口,在接口中新增方法,將不得不通知所有實現(xiàn)類來實現(xiàn)該新增方法。想象一下,你作為一個接口實現(xiàn)者,愿意莫名其妙的來實現(xiàn)一些自己壓根就沒用的方法么?這簡直就會讓人抓狂、瘋掉!難道就沒有解決辦法了么?
且慢,其實你不必抓狂。JDK1.8的出現(xiàn),解決了這些問題,從中引入了一種新的機制。JDK1.8中的接口支持在聲明方法的同時提供實現(xiàn),通過兩種方式可以完成這種操作:
JDK1.8允許在接口內(nèi)聲明靜態(tài)方法。
JDK1.8引入了一個新功能,叫默認(rèn)方法,通過默認(rèn)方法可以指定接口方法的默認(rèn)實現(xiàn)。
換句話說,接口中能夠提供方法的具體實現(xiàn)。因此,實現(xiàn)接口的類如果不顯示提供該方法的具體實現(xiàn),就會自動繼承默認(rèn)的實現(xiàn)。
默認(rèn)方法的出現(xiàn)可以使你輕松、平滑地進(jìn)行接口的優(yōu)化、升級。
實際上,從你使用JDK1.8開始就已經(jīng)使用了多個默認(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);
}
}
……
}
可以看到,這個新增的sort方法有方法體,由default修飾符修飾,這就是接口的默認(rèn)方法。
太開心了,在JDK1.8之后,接口居然可以這么輕松升級啦。
二、接口如何升級呢?
假如在一個繪圖庫中,存在一個接口Resizable,它定義了一個簡單的可縮放形狀必須支持的很多方法, 比如:setHeight、setWidth、getHeight、getWidth等。
此外,你還提供了幾個額外的實現(xiàn),如正方形、長方形。由于你的庫非常流行,被廣為流傳,你的一些用戶使用Resizable接口,創(chuàng)建了他們自己感興趣的實現(xiàn),比如橢圓。
接口Resizable如下:
public interface Resizable {
int getWidth();
int getHeight();
void setWidth(int width);
void setHeight(int height);
void setAbsoluteSize(int width, int height);
}
你的一位鐵桿用戶根據(jù)自身的需求實現(xiàn)了Resizable接口,創(chuàng)建了Ellipse類:
public class Ellipse implements Resizable {
…
}
他實現(xiàn)了一個處理各種Resizable形狀(包括Ellipse)的游戲:
庫上線使用幾個月之后,你收到很多請求,要求你更新Resizable的實現(xiàn),讓Square、Rectangle以及其他的形狀都能支持setRelativeSize方法。為了滿足這些新的需求,你不得不來升級你接口。
如果直接在接口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);
}
將會導(dǎo)致一系列的問題,不得不要求接口的實現(xiàn)者(庫用戶)強制來添加setAbsoluteSize方法的實現(xiàn),這肯定是不太現(xiàn)實的升級方法。
這時,我們想到了默認(rèn)方法,便可以解決上面這種問題。不但滿足了新用戶的需求,而且也兼容了老用戶。
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);
}
}
如果用戶對方法setRelativeSize的功能有新的想法,自己完全也可以重寫該方法。
完美,至此升級完成!
三、默認(rèn)方法的妙用
默認(rèn)方法的引入就是為了以兼容的方式解決像 Java API這樣的類庫的演進(jìn)問題的,如下圖所示:
簡而言之,升級接口中的方法是諸多問題的罪惡之源;一旦接口發(fā)生變化,實現(xiàn)這些接口的類往往也需要更新,提供新添方法的實現(xiàn)才能適配接口的變化。
現(xiàn)在你已經(jīng)了解了默認(rèn)方法是怎樣以兼容的方式來升級接口。除了上面的使用場景,還有其他場景也能利用這個新特性嗎?當(dāng)然有,你可以創(chuàng)建自己的接口,并為其提供默認(rèn)方法。接下來,就介紹使用默認(rèn)方法的兩種場景:
可選方法
多繼承
1. 可選方法
你很可能也碰到過這種情況,類實現(xiàn)了接口,不過卻刻意地將一些方法的實現(xiàn)留白。我們以Iterator接口為例來說。Iterator接口定義了hasNext、next,還定義了remove方法。
Java 8之前,由于用戶通常不會使用該方法,remove方法常被忽略。因此,實現(xiàn)Interator接口的類通常會為remove方法放置一個空的實現(xiàn),這些都是些毫無用處的模板代碼。
采用默認(rèn)方法之后,你可以為這種類型的方法提供一個默認(rèn)的實現(xiàn),這樣實體類就無需在自己的實現(xiàn)中顯式地提供一個空方法。比如,在Java 8中,Iterator接口就為remove方法提供了一個默認(rèn)實現(xiàn),如下所示:
interface Iterator<T> {
boolean hasNext();
T next();
default void remove() {
throw new UnsupportedOperationException();
}
}
通過這種方式,你可以減少無效的模板代碼。實現(xiàn)Iterator接口的每一個類都不需要再聲明一個空的remove方法了,因為它現(xiàn)在已經(jīng)有一個默認(rèn)的實現(xiàn)。
2. 多繼承
默認(rèn)方法讓之前無法想象的事兒以一種優(yōu)雅的方式得以實現(xiàn),即多繼承。這是一種讓類從多個來源重用代碼的能力,如圖下圖:
看到這里,不要誤認(rèn)為Java是支持多繼承的哦。切記:“Java的類只能繼承單一的類,但是一個類可以實現(xiàn)多接口?!?/p>
比如,我們設(shè)計一款游戲,需要定義多個具有不同特質(zhì)的性質(zhì)。有的形狀需要調(diào)整大小,但是不需要有旋轉(zhuǎn)的功能;有的需要能旋轉(zhuǎn)和移動,但是不需要調(diào)整大小。我們可以如何設(shè)計呢?這里就可以用到默認(rèn)方法了。
定義一個可旋轉(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);
}
}
定義一個可移動接口,并提供移動的默認(rèn)方法:
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);
}
}
定義一個可改變大小接口,并提供改變大小的默認(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)建不同的實體類,比如Monster可以移動、 旋轉(zhuǎn)和縮放。
public class Monster implements Rotatable, Moveable, Resizable {
//... 只需要給出所有抽象方法的實現(xiàn),不需要重復(fù)實現(xiàn)默認(rèn)方法
}
Monster繼承了rotateBy、moveHorizontally、moveVertically和setRelativeSize的實現(xiàn)。我們也可以直接調(diào)用不同的方法:
Monster m = new Monster();
//調(diào)用由Rotatable中繼承而來的rotateBy
m.rotateBy(180);
//調(diào)用由Moveable中繼承而來的moveVertically
m.moveVertically(10);
假設(shè)你現(xiàn)在需要聲明另一個類,它要能移動和旋轉(zhuǎn),但是不能縮放,比如說Sun。這時也無需復(fù)制粘貼代碼,你可以像下面這樣復(fù)用Moveable和Rotatable接口的默認(rèn)實現(xiàn)。
public class Sun implements Moveable, Rotatable {
…
}
四、總結(jié)
通過本篇文章,認(rèn)識了Java8新引入的默認(rèn)方法,也通過實例看到了這種默認(rèn)方法帶來的靈活性,更能夠輕松升級接口中的方法,所以此后若在項目中,需要對接口進(jìn)行改動,也可以想想是否可以使用默認(rèn)方法。