介面預設方法


到目前為止你可以看到,JDK8的Lambda不僅只是引入了Lambda語法,也考慮了如何善用既有API,除此之外,JDK8也試圖在既有API增加功能,讓開發者在遷移至JDK8平台的同時,馬上就有更多能搭配Lambda的強大API可使用。

這邊的問題是,這些API要放在哪?例如,像迭代物件時的forEach方法要放在哪呢?我們是可以把這些方法定義為Collections類別上的static方法,過去不少為增強Collection功能的第三方程式庫就是這麼做的,然而,JDK8希望這些API具備物件導向程式設計風格,在撰寫程式碼時也能更為流暢,因而將這些方法定義為Collections類別的static方法並不適合,也就是說,JDK8希望的風格會像是:

List<String> names = ...;
names.filter(s -> s.length() < 3)
     .forEach(out::println);

而不像是…

forEach(filter(names, s -> s.length() < 3), out::println);

前者的風格顯然在閱讀上較為流暢。只是,我們有辦法在List介面中增加像filter之類的方法嗎?如果用的是JDK7或先前的版本,答案當然是否定的!所有實作List介面的客戶端程式碼都會出錯,因為它們本來就沒有實作新增的那些方法。建立一個新的Collections2 API是個選項,不過現有的Collection API遍佈在全世界許多的程式庫中,要把這些既有的Collection API替換為新的Collections2會是個龐大任務,在JDK8釋出後,開發者應該不會想馬上用新的Collections2 API吧!

JDK8最後採取的策略是,直接演化interface的語法,在JDK8中,interface定義時可以加入預設實作,或者稱為預設方法(Default methods)。預設方法的實例之一,就是定義在Iterable介面的forEach方法:

package java.lang;
 
import java.util.Iterator;
import java.util.Objects;
import java.util.function.Consumer;
 
@FunctionalInterface
public interface Iterable<T> {
    Iterator<T> iterator();
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
}

Iterable的實作類別,必須實作iterator方法,這麼一來,API客戶端就可以直接使用forEach方法。例如,你可以如下撰寫程式碼:

List<String> names = ...;
names.forEach(out::println);

因為forEach方法本身已有實作,所以不會破壞 Iterable 現有的其它實作。預設方法令介面看來像是有抽象方法的抽象類別,不過不同點在於,預設方法中不能使用值域(Field)成員,因為介面本身不能定義值域成員,也就是不能預設方法中不能有直接變更狀態的流程。如下所示,你可以使用預設方法來實作 樣版方法(Template Method)模式,例如,你可以如下定義自己的Comparable介面:

public interface Comparable<T> {
    int compareTo(T that);
 
    default boolean lessThan(T that) {
        return compareTo(that) < 0;
    }
    default boolean lessOrEquals(T that) {
        return compareTo(that) <= 0;
    }
    default boolean greaterThan(T that) {
        return compareTo(that) > 0;
    }
    ...
}

如果有個Ball類別打算實作Comparable介面的話,就只需要實作compareTo方法:

public class Ball implements Comparable<Ball> {
    private int radius;
    ...
    public int compareTo(Ball that) {
        return this.radius - that.radius;
    }
}

這麼一來,每個Ball實例就可擁有Comparable介面定義的那些預設方法。因為類別可以實作多個介面,預設方法的新特性,讓你可以在某些介面中定義可共用的一些操作,如果有個類別需要某些可共用的操作,就只需要實作相關介面,並實作介面中未實作的抽象方法,那麼就可以混入(Mixin)這些共用的操作了。

JDK8之前不讓介面擁有預設方法是有原因的,因為實作介面是廣義的多重繼承,介面沒有實作時,類別與介面繼承時的方法來源在判斷上就會單純許多,介面在JDK8中允許有預設實作,引入了強大的威力,也引入了更多的複雜度,你得留意到底採用的是哪個方法版本。

就如同子類別可繼承父類別實作,介面也可以被繼承,而抽象方法或預設方法都會被繼承下來,在子介面中再以抽象方法重新定義一次父介面已定義的抽象方法,通常是為了文件化,這是過去經常看到的實踐(Practice),反正沒有實作,沒什麼實作版本上的問題。

父介面中的抽象方法,可以在子類別中以預設方法實作,父類別中的預設方法,可以在子介面中被新的預設方法重新定義。

如果父介面中有個預設方法,子介面中再度宣告與父介面預設方法相同的方法簽署,但沒有寫出default,也就是沒有方法實作,那麼子介面中該方法就直接重新定義了父類別中的預設方法實作為抽象方法。例如:

import java.util.Iterator;
import java.util.function.Consumer;

public interface BiIterable<T> extends Iterable<T> {
    Iterator<T> iterator();
    void forEach(Consumer<? super T> action);
    ...
}

在上面的例子中,BiIterableforEach()方法就沒有實作了,實作BiIterable的類別,必須實作forEach()方法。如果有兩個父介面都定義了相同方法簽署的預設方法,那麼會引發衝突。例如PartCanvas兩個介面若都有個defaultdraw()方法,而Lego介面繼承PartCanvas時,沒有重新定義draw(),就會發生編譯錯誤:

interface Lego inherits unrelated defaults for draw() from types Part and Canvas

解決的方式是明確重新定義draw(),無論是重新定義為抽象或預設方法,如果重新定義為預設方法時,想明呼叫某個父介面的draw()方法,必須使用介面名稱與super明確指定,例如:

public interface Lego extends Part, Canvas {
    default void draw() {
        Part.super.draw();
    }
}

類別在實作有預設方法的介面時,可以重新定義預設方法,無論是重新定義具體實作,或是將該方法標示為abstract,如果實作時有兩個介面都定義了相同方法簽署的預設方法,那麼會引發衝突,解決的方式是明確重新定義draw(),無論是重新定義為抽象或預設方法,如果重新定義為具體方法時,想明呼叫某個介面的draw()方法,也是得使用介面名稱與super明確指定。

如果類別實作的兩個介面擁有相同的父介面,其中一個介面重新定義了父介面的預設方法,而另一個介面沒有,那麼實作類別會採用重新定義了的版本。例如:

class LinkedList<E> implements List<E>, Queue<E>

Collection定義了removeAll()方法,List繼承自Collection重新以預設方法定義了removeAll()Queue繼承自Collection,沒有重新定義removeAll()方法,那麼LinkedList採用的版本,就是List中的removeAll()預設方法,而不是Collection中的removeAll()預設方法。

如果類別繼承了父類別同時實作某介面,而父類別中的方法與介面中的預設方法具有相同方法簽署,則採用父類別的方法定義。

簡單來說,類別中的定義優先於介面中的定義,如果有重新定義,就以重新定義的為主,必要時使用介面與super指定採用哪個預設方法。

JDK8除了讓介面可以定義預設方法之外,也開始允許在介面中定義靜態方法。