1. 程式人生 > >Effective Java 3rd 條目20 介面優於抽象類

Effective Java 3rd 條目20 介面優於抽象類

Java有兩個機制定義允許多個實現的型別:介面和抽象類。因為在Java8中引入了介面的預設方法(default method)[JLS 9.4.3],所以兩個機制都讓你可以為一些例項方法提供實現。主要的區別在於,為了實現由抽象類定義的型別,一個類必須是這個抽象類的子類。因為Java僅僅允許單繼承,抽象類的限制嚴重約束了作為型別定義的使用。定義所有必需方法和遵從通用規範的任何類,允許實現一個介面,而不管這個類在類層級的所處位置。

現存類可以容易地更新為實現一個新介面。你要做的所有事情是,如果必需的方法還不存在時新增它們,而且新增implements子句到類宣告中。例如,當許多存在類新增到平臺時,它們更新為實現Comparable、Iterable和Autocloseable介面。通常,現存類不能更新為擴充套件一個新抽象類。如果你想讓兩個類擴充套件同一個抽象類,你必須把它放到類層級的更高位置,這個位置是這兩個類的祖先。不幸的是,這可能造成類層次的間接傷害,即迫使新抽象類的所有子類,不管這是否合適,必須子類化它。

介面對於定義混入很理想的。大致地講,混入(mixin)是一個,除了它的“主要型別”,可實現型別,宣告它提供了可選型別。比如,Comparable是一個混入介面,讓一個類宣告,相應於其他可比較的可變物件,它的例項是有序的。這樣的介面叫做混入,因為它允許可選功能“混入”到型別的主要功能。由於同樣的原因,抽象類不能使用於定義混入:它們不能更新到現存類中,一個類不能有多於一個祖先,而且在類層級中沒有合理的位置插入一個混入。

介面為構建非層級型別框架留出餘地。型別層級對於組織一些事情是很棒的,但是其他事情不能整潔地放入到嚴格的層級中。例如,假設我們有代表歌手的介面和代表歌曲作家的介面:

public
interface Singer { AudioClip sing(Song s); } public interface Songwriter { Song compose(int chartPosition); }

在實際中,一些歌手也是歌曲作家。因為我們使用了介面而不是抽象類定義這些型別,對於單個型別,同時實現Singer和Songwriter是完全允許的。事實上,我們可以定義第三個介面,同時擴充套件了Singer和Songwriter,而且新增適合於組合的新方法:

public interface SingerSongwriter extends Singer
, Songwriter {
AudioClip strum(); void actSensitive(); }

你不總是需要這個靈活級別,但是當你需要的時候,介面是救命稻草。替代方法是一個膨脹的類層級,為每個支援的屬性組合提供一個單獨的類。如果這個型別系統中有n個屬性,那麼你可能不得不支援的 2 n 個可能的組合。這個叫組合爆炸。膨脹類層級可能導致膨脹類,膨脹類有僅僅不同於它們引數型別的許多方法,因為在類層級中沒有型別獲取通用行為。

通過包裝類習慣用法(條目18),介面使得增強安全強大的功能變得可能。如果你使用抽象類定義型別,那麼你讓想新增功能的程式設計師沒有選擇,只有使用繼承。最終的類是不那麼強大了而且比包裝類更加脆弱。

當依據其他介面方法,有一個介面方法的明顯實現,那麼考慮為程式設計師以預設方法的形式提供實現協助。作為這個技巧的例子,參考104頁的removeIf方法。如果你提供預設方法,確保為它們的繼承使用@implSpec Javadoc標籤文件化(條目19)。

你可以為預設方法提供多少實現協助,是有限制的。雖然許多介面指定Object方法的行為,比如equals和hashCode,但是你不允許為它們提供預設方法。而且,介面不允許包含例項域或者非公開靜態成員(除了私有靜態方法)。最終,你不能新增預設方法到你不能控制的介面。

然而,你可以結合介面和抽象類的優點,通過提供一個和介面匹配的抽象骨架實現(skeletal implementation)類。介面定義了這個型別,或許提供了一些預設方法,而骨架實現類,在基元介面方法上,實現了剩下的非基元介面方法。擴充套件骨架實現把實現介面的大部分工作拿出來了。這叫模板方法(Template Method)模式[Gamma95]。

按照慣例,骨架實現叫做AbstractInterface,這裡面Interface是它們實現介面的名字。例如,Collections框架提供了一個骨架實現,支援每個主要集合介面:AbstractCollection、AbstractSet、AbstractList和 AbstractMap。有爭議地,它們叫SkeletalCollection、SkeletalSet、SkeletalList和SkeletalMap是說的通的,但是Abstract慣例現在牢固地建立了。當你合理設計時,骨架實現(不管單獨抽象類,還是僅僅包含介面的預設方法)可以讓程式設計師容易地提供他們自己的介面實現。例如,以下是一個靜態工廠方法,在AbstractList之上包含了一個完整和完全功能List實現:

// 建立於骨架實現上的具體實現
static List<Integer> intArrayAsList(int[] a) {
     Objects.requireNonNull(a);

    // 菱形操作子只有在在Java9和之後是合法的
    // 如果你使用之前的版本,指定<Integer>
    return new AbstractList<>() {
        @Override public Integer get(int i) { 
            return a[i]; // 自動裝箱(條目6) 
        }

        @Override public Integer set(int i, Integer val) { 
            int oldVal = a[i]; 
            a[i] = val; // 自動拆箱 
            return oldVal; // 自動裝箱
        }

        @Override public int size() { 
            return a.length; 
        }
    };
}

當你考慮到List實現為你所做的所有事情,這個例子是一個骨架實現能力的令人印象深刻的展示。順便說一下,這個例子是一個Adapter[Gamma95],它允許int佇列看成是Integer例項的列表。因為int值和Integer例項(裝箱和拆箱)之間來回轉換,它的效能不是很好。注意,實現以匿名類的方式出現(條目24)。

骨架實現類的漂亮之處在於,它們提供了抽象類的所有實現協助,當抽象類作為型別定義時沒有由它們強加的嚴重約束。對於有骨架實現類的大多數介面實現者,擴充套件這個類是一個明顯優勢的選擇,但是嚴格來講它是可選的。如果一個類不能夠擴充套件骨架類,這個類總能直接實現這個介面。這個類仍舊從介面自身中的任何預設方法中獲益。此外,骨架實現仍舊輔助實現者的任務。實現這個介面的類可以轉發介面方法呼叫到一個私有內部類的包含例項,這個內部類擴充套件了骨架實現。這個技巧,叫做模擬多繼承(simulated multiple inheritance),是和條目18中討論的包裝類習慣用法密切相關。它提供了多繼承的許多好處,而且避免了缺陷。

編寫骨架實現是一個相當簡單的過程,雖然有點單調。首先,研究這個介面,然後決定哪些方法是基元,依據這些,可以實現其他方法。接下來,為直接可以在基元上實現的所有方法,提供介面預設方法,但是回憶到,你可以不提供Object的預設方法,比如equals和hashCode。如果基元和預設方法包括了這個介面,你就完成了,而且對於骨架實現類來說沒必要。這個類可能包含了任意一個非公開域和適合這個任務的方法。

作為一個簡單的類子,考慮Map.Entry介面。顯而易見的基元是getKey、getValue和setValue(可選)。這個介面指定了equals和hashCode的行為,有一個依據基元的toString明顯實現。因為你不允許為Object方法提供預設實現,所以所有實現放置在骨架實現類中:

// 骨架實現類 
public abstract class AbstractMapEntry<K,V> implements Map.Entry<K,V> { 
    // 可更改對映中的實體必須覆寫這個方法
    @Override public V setValue(V value) {
        throw new UnsupportedOperationException(); 
    } 
    // 實現Map.Entry.equals的通用協定
    @Override public boolean equals(Object o) {
        if (o == this) return true; 
        if (!(o instanceof Map.Entry)) return false; 
        Map.Entry<?,?> e = (Map.Entry) o; 
        return Objects.equals(e.getKey(), getKey())
            && Objects.equals(e.getValue(), getValue());
    }

    // 實現Map.Entry.hashCode的通用協定
    @Override public int hashCode() { 
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); 
    }

    @Override public String toString() { 
        return getKey() + "=" + getValue(); 
    }
}

注意到,這個骨架實現不能在Map.Entry介面中實現,或者作為一個子介面,因為預設方法不允許覆寫Object方法,比如equals、hashCode和toString。

因為骨架實現是為繼承設計的,你應該遵從條目19中的所有設計和文件規範。為簡略起見,前面例子中省略了文件註釋,但是在一個骨架實現中好的文件是決定必要的,不管介面中還是單獨抽象類中它包含了預設方法。

介面實現的一個小變體是由AbstractMap.SimpleEntry例證的簡單實現。一個簡單實現像一個骨架實現,它實現一個介面而且為繼承設計,但是不同之處在於它不是抽象的:它是最簡單的可運作的實現。你可以按照這種樣子使用它,或者環境允許時子類化它。

總而言之,一個介面通常是定義允許多實現的一個型別的最好方法。如果你匯出一個非顯然的介面,那麼你應該仔細考慮為它匹配一個骨架實現。在可能的範圍內,你應該通過介面預設方法提供骨架實現,以便這個介面的所有實現可以使用它。也就是說,介面限制通常要求骨架實現以抽象類的方式出現。