Effective Java 第三版——20. 接口優於抽象類


Tips
《Effective Java, Third Edition》一書英文版已經出版,這本書的第二版想必很多人都讀過,號稱Java四大名著之一,不過第二版2009年出版,到現在已經將近8年的時間,但隨着Java 6,7,8,甚至9的發布,Java語言發生了深刻的變化。
在這里第一時間翻譯成中文版。供大家學習分享之用。

Effective Java, Third Edition

20. 接口優於抽象類

Java有兩種機制來定義允許多個實現的類型:接口和抽象類。 由於在Java 8 [JLS 9.4.3]中引入了接口的默認方法(default methods ),因此這兩種機制都允許為某些實例方法提供實現。 一個主要的區別是要實現由抽象類定義的類型,類必須是抽象類的子類。 因為Java只允許單一繼承,所以對抽象類的這種限制嚴格限制了它們作為類型定義的使用。 任何定義所有必需方法並服從通用約定的類都可以實現一個接口,而不管類在類層次結構中的位置。

現有的類可以很容易地進行改進來實現一個新的接口。 你只需添加所需的方法(如果尚不存在的話),並向類聲明中添加一個implements子句。 例如,當Comparable, Iterable, 和Autocloseable接口添加到Java平台時,很多現有類需要實現它們來加以改進。 一般來說,現有的類不能改進以繼承一個新的抽象類。 如果你想讓兩個類繼承相同的抽象類,你必須把它放在類型層級結構中的上面位置,它是兩個類的祖先。 不幸的是,這會對類型層級結構造成很大的附帶損害,迫使新的抽象類的所有后代對它進行子類化,無論這些后代類是否合適。

接口是定義混合類型(mixin)的理想選擇。 一般來說,mixin是一個類,除了它的“主類型”之外,還可以聲明它提供了一些可選的行為。 例如,Comparable是一個類型接口,它允許一個類聲明它的實例相對於其他可相互比較的對象是有序的。 這樣的接口被稱為類型,因為它允許可選功能被“混合”到類型的主要功能。 抽象類不能用於定義混合類,這是因為它們不能被加載到現有的類中:一個類不能有多個父類,並且在類層次結構中沒有合理的位置來插入一個類型。

接口允許構建非層級類型的框架。 類型層級對於組織某些事物來說是很好的,但是其他的事物並不是整齊地落入嚴格的層級結構中。 例如,假設我們有一個代表歌手的接口,另一個代表作曲家的接口:

public interface Singer {
    AudioClip sing(Song s);
}

public interface Songwriter {
    Song compose(int chartPosition);
}

在現實生活中,一些歌手也是作曲家。 因為我們使用接口而不是抽象類來定義這些類型,所以單個類實現歌手和作曲家兩個接口是完全允許的。 事實上,我們可以定義一個繼承歌手和作曲家的第三個接口,並添加適合於這個組合的新方法:

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

    void actSensitive();
}

你並不總是需要這種靈活性,但是當你這樣做的時候,接口是一個救星。 另一種方法是對於每個受支持的屬性組合,包含一個單獨的類的臃腫類層級結構。 如果類型系統中有n個屬性,則可能需要支持2n種可能的組合。 這就是所謂的組合爆炸(combinatorial explosion)。 臃腫的類層級結構可能會導致具有許多方法的臃腫類,這些方法僅在參數類型上有所不同,因為類層級結構中沒有類型來捕獲通用行為。

接口通過包裝類模式確保安全的,強大的功能增強成為可能(條目 18)。 如果使用抽象類來定義類型,那么就讓程序員想要添加功能,只能繼承。 生成的類比包裝類更弱,更脆弱。

當其他接口方法有明顯的接口方法實現時,可以考慮向程序員提供默認形式的方法實現幫助。 有關此技術的示例,請參閱第104頁的removeIf方法。如果提供默認方法,請確保使用@implSpec Javadoc標記(條目19)將它們文檔說明為繼承。

使用默認方法可以提供實現幫助多多少少是有些限制的。 盡管許多接口指定了Object類中方法(如equalshashCode)的行為,但不允許為它們提供默認方法。 此外,接口不允許包含實例屬性或非公共靜態成員(私有靜態方法除外)。 最后,不能將默認方法添加到不受控制的接口中。

但是,你可以通過提供一個抽象的骨架實現類(abstract skeletal implementation class)來與接口一起使用,將接口和抽象類的優點結合起來。 接口定義了類型,可能提供了一些默認的方法,而骨架實現類在原始接口方法的頂層實現了剩余的非原始接口方法。 繼承骨架實現需要大部分的工作來實現一個接口。 這就是模板方法設計模式[Gamma95]。

按照慣例,骨架實現類被稱為AbstractInterface,其中Interface是它們實現的接口的名稱。 例如,集合框架( Collections Framework)提供了一個框架實現以配合每個主要集合接口:AbstractCollectionAbstractSetAbstractListAbstractMap。 可以說,將它們稱為SkeletalCollectionSkeletalSetSkeletalListSkeletalMap是有道理的,但是現在已經確立了抽象約定。 如果設計得當,骨架實現(無論是單獨的抽象類還是僅由接口上的默認方法組成)可以使程序員非常容易地提供他們自己的接口實現。 例如,下面是一個靜態工廠方法,在AbstractList的頂層包含一個完整的功能齊全的List實現:

// Concrete implementation built atop skeletal implementation

static List<Integer> intArrayAsList(int[] a) {

    Objects.requireNonNull(a);

    // The diamond operator is only legal here in Java 9 and later

    // If you're using an earlier release, specify <Integer>

    return new AbstractList<>() {

        @Override public Integer get(int i) {

            return a[i];  // Autoboxing ([Item 6](https://www.safaribooksonline.com/library/view/effective-java-third/9780134686097/ch2.xhtml#lev6))

        }

        @Override public Integer set(int i, Integer val) {

            int oldVal = a[I];

            a[i] = val;     // Auto-unboxing

            return oldVal;  // Autoboxing

        }

        @Override public int size() {

            return a.length;

        }

    };

}

當你考慮一個List實現為你做的所有事情時,這個例子是一個骨架實現的強大的演示。 順便說一句,這個例子是一個適配器(Adapter )[Gamma95],它允許一個int數組被看作Integer實例列表。 由於int值和整數實例(裝箱和拆箱)之間的來回轉換,其性能並不是非常好。 請注意,實現采用匿名類的形式(條目 24)。

骨架實現類的優點在於,它們提供抽象類的所有實現的幫助,而不會強加抽象類作為類型定義時的嚴格約束。對於具有骨架實現類的接口的大多數實現者來說,繼承這個類是顯而易見的選擇,但它不是必需的。如果一個類不能繼承骨架的實現,這個類可以直接實現接口。該類仍然受益於接口本身的任何默認方法。此外,骨架實現類仍然可以協助接口的實現。實現接口的類可以將接口方法的調用轉發給繼承骨架實現的私有內部類的包含實例。這種被稱為模擬多重繼承的技術與條目 18討論的包裝類模式密切相關。它提供了多重繼承的許多好處,同時避免了缺陷。

編寫一個骨架的實現是一個相對簡單的過程,雖然有些乏味。 首先,研究接口,並確定哪些方法是基本的,其他方法可以根據它們來實現。 這些基本方法是你的骨架實現類中的抽象方法。 接下來,為所有可以直接在基本方法之上實現的方法提供接口中的默認方法,回想一下,你可能不會為諸如Object類中equalshashCode等方法提供默認方法。 如果基本方法和默認方法涵蓋了接口,那么就完成了,並且不需要骨架實現類。 否則,編寫一個聲明實現接口的類,並實現所有剩下的接口方法。 為了適合於該任務,此類可能包含任何的非公共屬性和方法。

作為一個簡單的例子,考慮一下Map.Entry接口。 顯而易見的基本方法是getKey,getValue和(可選的)setValue。 接口指定了equalshashCode的行為,並且在基本方面方面有一個toString的明顯的實現。 由於不允許為Object類方法提供默認實現,因此所有實現均放置在骨架實現類中:

// Skeletal implementation class

public abstract class AbstractMapEntry<K,V>

        implements Map.Entry<K,V> {

    // Entries in a modifiable map must override this method

    @Override public V setValue(V value) {

        throw new UnsupportedOperationException();

    }

    // Implements the general contract of 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());

    }

    // Implements the general contract of Map.Entry.hashCode

    @Override public int hashCode() {

        return Objects.hashCode(getKey())

             ^ Objects.hashCode(getValue());

    }

    @Override public String toString() {

        return getKey() + "=" + getValue();

    }

}

請注意,這個骨架實現不能在Map.Entry接口中實現,也不能作為子接口實現,因為默認方法不允許重寫諸如equalshashCodetoStringObject類方法。

由於骨架實現類是為了繼承而設計的,所以你應該遵循條目 19中的所有設計和文檔說明。為了簡潔起見,前面的例子中省略了文檔注釋,但是好的文檔在骨架實現中是絕對必要的,無論它是否包含 一個接口或一個單獨的抽象類的默認方法。

與骨架實現有稍許不同的是簡單實現,以AbstractMap.SimpleEntry為例。 一個簡單的實現就像一個骨架實現,它實現了一個接口,並且是為了繼承而設計的,但是它的不同之處在於它不是抽象的:它是最簡單的工作實現。 你可以按照情況使用它,也可以根據情況進行子類化。

總而言之,一個接口通常是定義允許多個實現的類型的最佳方式。 如果你導出一個重要的接口,應該強烈考慮提供一個骨架的實現類。 在可能的情況下,應該通過接口上的默認方法提供骨架實現,以便接口的所有實現者都可以使用它。 也就是說,對接口的限制通常要求骨架實現類采用抽象類的形式。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM