1.簡述
在Java8之前隧期,Java程序接口是將相關方法按照約定組合到一起的方式按声。實現(xiàn)接口的類必須為接口中定義的每個方法提供一個實現(xiàn)燃少,或者從父類中繼承它的實現(xiàn)邑贴。但是席里,一旦類庫的設計者需要更新接口,向其中加入新的方法拢驾,這種方式就會出現(xiàn)問題〗贝牛現(xiàn)實情況是,現(xiàn)存的實體類往往不在接口設計者的控制范圍之內繁疤,這些實體類為了適配新的接口約定也需要進行修改咖为。由于Java8的API在現(xiàn)存的接口上引入了非常多的新方法,這種變化帶來的問題也愈加嚴重稠腊。
在Java8中為了解決這個問題引入了一種新的機制躁染。Java8中的接口現(xiàn)在支持在聲明方法的同時提供實現(xiàn)。有兩種方式可以完成這種操作架忌。其一吞彤,Java8允許在接口內聲明靜態(tài)方法。其二鳖昌,Java8引入了一個新功能备畦,叫默認方法。通過默認方法许昨,即使實現(xiàn)接口的方法也可以自動繼承默認的實現(xiàn)懂盐,你可以讓你的接口可以平滑地進行接口的進化和演進。比如我們的List接口中的sort方法是java8中全新的方法,定義如下:
default void sort(Comparator<? super E> c){
Collections.sort(this, c);
}
在方法有個default修飾符用來表示這是默認方法糕档。
2.進化的API
為了理解為什么一旦API發(fā)布之后莉恼,它的演進就變得非常困難拌喉,我們假設你是一個流行Java繪圖庫的設計者(為了說明本節(jié)的內容,我們做了這樣的假想)俐银。你的庫中包含了一個Resizable接口尿背,它定義了一個簡單的可縮放形狀必須支持的很多方法,比如:setHeight捶惜、 setWidth田藐、getHeight、getWidth以及setAbsoluteSize吱七。此外汽久,你還提供了幾個額外的實現(xiàn)(out-of-boximplementation),如正方形踊餐、長方形景醇。由于你的庫非常流行,你的一些用戶使用Resizable接口創(chuàng)建了他們自己感興趣的實現(xiàn)吝岭,比如橢圓三痰。
發(fā)布API幾個月之后,你突然意識到Resizable接口遺漏了一些功能窜管。比如散劫,如果接口提供一個setRelativeSize方法,可以接受參數(shù)實現(xiàn)對形狀的大小進行調整微峰,那么接口的易用性會更好舷丹。你會說這看起來很容易啊:為Resizable接口添加setRelativeSize方法蜓肆,再更新Square和Rectangle的實現(xiàn)就好了颜凯。不過,事情并非如此簡單仗扬!你要考慮已經使用了你接口的用戶症概,他們已經按照自身的需求實現(xiàn)了Resizable接口,他們該如何應對這樣的變更呢早芭?非常不幸彼城,你無法訪問,也無法改動他們實現(xiàn)了Resizable接口的類退个。這也是Java庫的設計者需要改進JavaAPI時面對的問題募壕。讓我們以一個具體的實例為例,深入探討修改一個已發(fā)布接口的種種后果语盈。
2.1初始化版本的API
Resizable最開始的版本如下:
public interface Resizable{
int getWidth();
int getHeight();
void setWidth(int width);
void setHeight(int height);
void setAbsoluteSize(int width, int height);
}
這時候有一位用戶實現(xiàn)了你的Resizable接口舱馅,創(chuàng)建了Ellipse類:
public class Ellipse implements Resizable {
@Override
public int getWidth() {
return 0;
}
@Override
public int getHeight() {
return 0;
}
@Override
public void setWidth(int width) {
}
@Override
public void setHeight(int height) {
}
@Override
public void setAbsoluteSize(int width, int height) {
}
}
2.2第二版本API
庫上線使用幾個月之后,你收到很多請求刀荒,要求你更新Resizable的實現(xiàn)代嗤,所以你更新了一個方法棘钞。
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);//第二版本API
}
接下來用戶便會面臨很多問題。首先干毅,接口現(xiàn)在要求它所有的實現(xiàn)類添加setRelativeSize方法的實現(xiàn)宜猜。但我們剛才的用戶最初實現(xiàn)的Ellipse類并未包含setRelativeSize方法。向接口添加新方法是二進制兼容的硝逢,這意味著如果不重新編譯該類姨拥,即使不實現(xiàn)新的方法,現(xiàn)有類的實現(xiàn)依舊可以運行趴捅。但是這種情況少之又少垫毙,基本項目每次發(fā)布時都會重新編譯,所以必定會報錯拱绑。
最后,更新已發(fā)布API會導致后向兼容性問題丽蝎。這就是為什么對現(xiàn)存API的演進猎拨,比如官方發(fā)布的Java.Collection.API,會給用戶帶來麻煩屠阻。當然红省,還有其他方式能夠實現(xiàn)對API的改進,但是都不是明智的選擇国觉。比如吧恃,你可以為你的API創(chuàng)建不同的發(fā)布版本,同時維護老版本和新版本麻诀,但這是非常費時費力的痕寓,原因如下。其一蝇闭,這增加了你作為類庫的設計者維護類庫的復雜度呻率。其次,類庫的用戶不得不同時使用一套代碼的兩個版本呻引,而這會增大內存的消耗礼仗,延長程序的載入時間,因為這種方式下項目使用的類文件數(shù)量更多了逻悠。
這就是我們默認方法所要做的工作元践。它讓我們的類庫設計者放心地改進應用程序接口,無需擔憂對遺留代碼的影響童谒。
3.詳解默認方法
經過前述的介紹单旁,我們已經了解了向已發(fā)布的API添加方法,會對我們現(xiàn)存的代碼會造成多大的危害惠啄。默認方法是Java8中引入的一個新特性慎恒,依靠他我們可以在實現(xiàn)類中不用提供實現(xiàn)任内。
我們要使用我們的默認方法非常簡單,只需要在我們要實現(xiàn)的方法簽名前面添加default修飾符進行修飾融柬,并像類中聲明的其他方法一樣包含方法體死嗦。如下面的接口一樣:
public interface Sized {
int size();
default boolean isEmpty(){
return size() == 0;
}
}
這樣任何一個實現(xiàn)了Sized接口的類都會自動繼承isEmpty的實現(xiàn)。
3.1默認方法的使用模式
3.1.1可選方法
你有時候會碰到這種情況粒氧,類實現(xiàn)了接口越除,不過卻可以將一些方法的實現(xiàn)留白。比如我們Iterator接口外盯,我們一般不會去實現(xiàn)remove方法摘盆,經常實現(xiàn)都會留白,在Java8中為了解決這種辦法回味我們的remove方法添加默認的實現(xiàn),如下:
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
}
通過這種方式饱苟,我們可以減少無效的模板代碼孩擂。實現(xiàn)Iterator接口的每一個類都不需要再次實現(xiàn)remove的模板方法了。
3.1.2多繼承
默認方法讓之前的Java是不支持多繼承箱熬,但是默認方法的出現(xiàn)讓多繼承在java中變得可能了类垦。
Java的類只能繼承單一的類,但是一個類可以實現(xiàn)多接口城须。要確認也很簡單蚤认,下面是Java API中對ArrayList類的定義:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable,
Serializable, Iterable<E>, Collection<E> {
}
3.1.3沖突問題
我們知道Java語言中一個類只能繼承一個父類,但是一個類可以實現(xiàn)多個接口糕伐。隨著默認方法在Java8中引入砰琢,有可能出現(xiàn)一個類繼承了多個方法而它們使用的卻是同樣的函數(shù)簽名。這種情況下良瞧,類會選擇使用哪一個函數(shù)陪汽?在實際情況中,雖然這樣的沖突很難發(fā)生莺褒,但是一旦發(fā)生掩缓,就必須要規(guī)定一套約定來處理這些沖突。這一節(jié)中遵岩,我們會介紹Java編譯器如何解決這種潛在的沖突你辣。
public interface A {
default void hello(){
System.out.println("i am A");
}
}
interface B extends A{
default void hello(){
System.out.println("i am B");
}
}
class C implements A,B{
public static void main(String[] args) {
new C().hello();
}
}
上面的代碼會輸出i am B。為什么呢尘执?我們下面有三個規(guī)則:
- 類中的方法優(yōu)先級最高舍哄。類或父類中的聲明的方法的優(yōu)先級高于任何聲明為默認方法的優(yōu)先級。
- 如果無法依據(jù)第一條進行判斷誊锭,那么子接口的優(yōu)先級更高:函數(shù)簽名相同時表悬,優(yōu)先選擇擁有最具體實現(xiàn)的默認方法的接口。如果B繼承了A丧靡,那么B就比A的更具體蟆沫。
- 最后籽暇,如果還是無法判斷,繼承了多個接口的類必須通過顯示覆蓋和調用期望的方法饭庞,顯式地選擇使用哪一個默認方法的實現(xiàn)戒悠。
接下來舉幾個例子
public interface A {
default void hello(){
System.out.println("i am A");
}
}
interface B extends A{
default void hello(){
System.out.println("i am B");
}
}
class D implements A{
public void hello(){
System.out.println("i am D");
}
}
class C extends D implements A,B{
public static void main(String[] args) {
new C().hello();
}
}
上面會輸出D,遵循我們的第一條原則舟山,類中的方法優(yōu)先級最高绸狐。
public interface A {
default void hello(){
System.out.println("i am A");
}
}
interface B {
default void hello(){
System.out.println("i am B");
}
}
class C implements A,B{
public static void main(String[] args) {
new C().hello();
}
}
上面代碼會出現(xiàn)編譯錯誤:Error:(19, 1) java: 類 java8.C從類型 java8.A 和 java8.B 中繼承了hello() 的不相關默認值,這個時候必須利用第三條,顯式得去調用父類的接口:
class C implements A,B{
public void hello(){
B.super.hello();
}
public static void main(String[] args) {
new C().hello();
}
}