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

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

Effective Java, Third Edition

20. 接口優於抽象類

Java有兩種機制來定義容許多個實現的類型:接口和抽象類。 因爲在Java 8 [JLS 9.4.3]中引入了接口的默認方法(default methods ),所以這兩種機制都容許爲某些實例方法提供實現。 一個主要的區別是要實現由抽象類定義的類型,類必須是抽象類的子類。 由於Java只容許單一繼承,因此對抽象類的這種限制嚴格限制了它們做爲類型定義的使用。 任何定義全部必需方法並服從通用約定的類均可以實現一個接口,而無論類在類層次結構中的位置。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)。 若是使用抽象類來定義類型,那麼就讓程序員想要添加功能,只能繼承。 生成的類比包裝類更弱,更脆弱。ide

當其餘接口方法有明顯的接口方法實現時,能夠考慮向程序員提供默認形式的方法實現幫助。 有關此技術的示例,請參閱第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爲例。 一個簡單的實現就像一個骨架實現,它實現了一個接口,而且是爲了繼承而設計的,可是它的不一樣之處在於它不是抽象的:它是最簡單的工做實現。 你能夠按照狀況使用它,也能夠根據狀況進行子類化。

總而言之,一個接口一般是定義容許多個實現的類型的最佳方式。 若是你導出一個重要的接口,應該強烈考慮提供一個骨架的實現類。 在可能的狀況下,應該經過接口上的默認方法提供骨架實現,以便接口的全部實現者均可以使用它。 也就是說,對接口的限制一般要求骨架實現類採用抽象類的形式。

相關文章
相關標籤/搜索