1.簡述
在Java8之前,Java程序接口是將相關(guān)方法按照約定組合到一起的方式。實現(xiàn)接口的類必須為接口中定義的每個方法提供一個實現(xiàn),或者從父類中繼承它的實現(xiàn)。但是,一旦類庫的設計者需要更新接口,向其中加入新的方法,這種方式就會出現(xiàn)問題。現(xiàn)實情況是,現(xiàn)存的實體類往往不在接口設計者的控制范圍之內(nèi),這些實體類為了適配新的接口約定也需要進行修改。由于Java8的API在現(xiàn)存的接口上引入了非常多的新方法,這種變化帶來的問題也愈加嚴重。
在Java8中為了解決這個問題引入了一種新的機制。Java8中的接口現(xiàn)在支持在聲明方法的同時提供實現(xiàn)。有兩種方式可以完成這種操作。其一,Java8允許在接口內(nèi)聲明靜態(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é)的內(nèi)容,我們做了這樣的假想)。你的庫中包含了一個Resizable接口,它定義了一個簡單的可縮放形狀必須支持的很多方法,比如:setHeight、 setWidth、getHeight、getWidth以及setAbsoluteSize。此外,你還提供了幾個額外的實現(xiàn)(out-of-boximplementation),如正方形、長方形。由于你的庫非常流行,你的一些用戶使用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庫的設計者需要改進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,會給用戶帶來麻煩。當然,還有其他方式能夠?qū)崿F(xiàn)對API的改進,但是都不是明智的選擇。比如,你可以為你的API創(chuàng)建不同的發(fā)布版本,同時維護老版本和新版本,但這是非常費時費力的,原因如下。其一,這增加了你作為類庫的設計者維護類庫的復雜度。其次,類庫的用戶不得不同時使用一套代碼的兩個版本,而這會增大內(nèi)存的消耗,延長程序的載入時間,因為這種方式下項目使用的類文件數(shù)量更多了。
這就是我們默認方法所要做的工作。它讓我們的類庫設計者放心地改進應用程序接口,無需擔憂對遺留代碼的影響。
3.詳解默認方法
經(jīng)過前述的介紹,我們已經(jīng)了解了向已發(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方法,經(jīng)常實現(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的更具體。
- 最后,如果還是無法判斷,繼承了多個接口的類必須通過顯示覆蓋和調(diào)用期望的方法,顯式地選擇使用哪一個默認方法的實現(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() 的不相關(guān)默認值,這個時候必須利用第三條,顯式得去調(diào)用父類的接口:
class C implements A,B{
public void hello(){
B.super.hello();
}
public static void main(String[] args) {
new C().hello();
}
}