Effective Java(三)

三、類和接口

1. 使類和成員的可訪問性最小化

        要區別設計良好的模塊與設計不好的模塊,最重要的因素在於:這個模塊對於外界的其他模塊而言,是否隱藏其內部數據和其他實現細節。一個模塊不需要知道其他模塊的內部工作情況,這個概念被稱爲信息隱藏封裝,是軟件設計的基本原則之一。

        訪問控制(類、接口和成員的可訪問性)的第一原則:儘可能地使每個類或者成員不被外界訪問,即應使用與你正在編寫的軟件對應功能相一致的,儘可能最小的訪問級別

        公有的靜態final域要麼包含基本的類型的值,要麼包含指向不可變對象的引用。如果final域包含可變對象的引用,它變具有非final域的所有缺點。雖然引用本身不能被修改,但是它所被引用的對象卻可以被修改,這會導致災難性的後果。

        一種常見的錯誤:類具有公有的靜態final數據域,或者返回這種域的訪問方法,這幾乎總是錯誤的。原因是:長度非零的數組總是可變的。

//OBJECTS是不可變的,但它所指的對象卻是可變的,這點特別容易被忽視,造成安全漏洞
public static final Thing[] VALUES = {...};

//修改辦法一
private static final Thing[] PRIVATE_VALUES = {...};
public static final List<Thing> VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));

//修改辦法二
private static final Thing[] PRIVATE_VALUES = {...};
public static final Thing[] values() {
    return PRIVATE_VALUES.clone();

        除了公有靜態final域的特殊情形之外,公有類都不應該包含公有域。並且要確保公有靜態final域所引用的對象都是不可變的。

2. 在公有類中使用訪問方法而非公有域

        公有類永遠不應該暴露可變的域。

        在公有類中,將數據域設置成私有域,然後使用公有的訪問方法(getter)和公有的設值方法(setter)去訪問。

3. 使可變性最小化

        不可變類指的是實例不能被修改的類。每個實例中包含的所有信息都必須在創建該實例的時候就提供,並在對象的整個生命週期內固定不變。Java平臺類庫中包含許多不可變的類,有String、BigInteger、BigDecimal、基本類型包裝類。

        存在不可變類的理由:不可變的類比可變類更加易於設計、實現和使用,它們不易出錯,且更加安全。

爲了使類不可變,要遵循下面5條原則:

(1)不要提供任何會修改對象狀態的方法

(2)保證類不會被擴展

(3)使所有的域都是final的

(4)使所有的域都成爲私有的

(5)確保對於任何可變組件的互斥訪問

不可變類的優點:

  • 不可變對象比較簡單
  • 不可變對象本質上是線程安全的,它們不要求同步
  • 不可變對象可以被自由地共享,不需要進行保護性拷貝
  • 不可變對象爲其他對象提供了大量的構件

不可變類的缺點:

  • 對於每個不同的值都需要一個單獨的對象。在特定情況下存在潛在的性能問題。當把較大的值對象做成不可變類時,若其會對性能造成影響,可以爲它提供相應的可變配套類。

        創建這種對象的代價可能很高,特別是對於大型對象的情形。對於大型的對象,最好的辦法是提供一個公有的可變配套類,如String的公有配套類StringBuilder。

        爲了確保不可變性,類絕對不允許自身被子類化。除了“使類成爲final的”這種方法外,還可以讓類的所有構造器都變成私有的或者包級私有的,並添加公有的靜態工廠來代替公有的構造器

        堅決不要爲每個get方法編寫一個相應的set方法。除非有很好的理由要讓類成爲可變的類,否則就應該是不可變的。

4. 複合優先於繼承

        對普通的具體類進行跨越包邊界的繼承,是非常危險的。

  • 繼承打破了封裝性。子類依賴於超類中特定功能的實現細節。超類的實現有可能會隨着發行版本的不同而有所變化,如果真的發生了變化,子類可能會遭到破壞。因此,子類必須要跟着超類的更新而演變,除非超類是專門爲了擴展而設計的,且具有很好的文檔說明。
  • 另一個危險的原因來源於覆蓋(overriding)動作。如果在擴展一個類的時候,僅僅是添加新的方法,而不覆蓋現有的方法,你可能會認爲這是安全的。但是,如果超類在後續的發行版本中獲得了一個新的方法,並且不幸的是,你給子類提供了一個簽名相同但返回類型不同的方法,這樣的子類將無法通過編譯。

        注意:只有當子類真正是超類的子類型時,才適合繼承。換言之,對於兩個類A和B,只有當兩者之間確實存在“is-a”關係的時候,類B才應該擴展類A。

        採用複合則可以避免上述問題。複合是不用擴展現有的類,而是在新的類中增加一個私有域,它引用現有類的一個實例。新類中的每個實例方法都可以調用被包含的現有類實例中對應的方法,並返回它的結果。這被稱爲轉發。新類中的方法被稱爲轉發方法這樣得到的類非常穩固,它不依賴於現有類的實現細節。即使現有的類添加了新的方法,也不影響新的類。

5. 要麼就爲繼承而設計,並提供文檔說明,要麼就禁止繼承

        對不是爲了繼承而設計、並且沒有文檔說明的“外來”類進行子類化是很危險的。
        對於爲了繼承而設計的類,該類的文檔必須要精確地描述覆蓋每個方法所帶來的影響。即該類必須有文檔說明它可覆蓋的方法的自用性。對於每個公有的或受保護的方法或構造器,它的文檔必須指明該方法或構造器調用了哪些可覆蓋的方法,是以什麼順序調用的,每個調用的結果又是如何影響後續的處理過程的。

        爲了允許繼承,類還必須遵守其他一些約束:構造器決不能調用可被覆蓋的方法,無論是直接調用還是間接調用。如果違反了這條規則,很有可能導致程序失敗。

        對於普通的具體類,它們既不是final的,也不是爲了子類化而設計和編寫文檔的,繼承它們是非常危險的,因爲每次對這種類進行修改,從這個類擴展得到的客戶類都有可能遭到破壞。對這個問題的最佳解決方案是:對於那些並非爲了安全地進行子類化而設計和編寫文檔的類,要禁止子類化。

有兩種方法可以禁止子類化:

(1)把這個類聲明爲final的

(2)把所有的構造器都變成私有的,或者包級私有的,並增加一些公有的靜態工廠來替代構造器。

        如果禁止繼承可能會帶來不便,或者認爲必須允許從這樣的類繼承,一種合理的辦法是確保這個類永遠不會調用它的任何可覆蓋的方法,並在文檔中說明這一點。換言之,完全消除這個類中可覆蓋方法的自用特性。這樣做之後,就可以創建“能夠安全地進行子類化”的類。覆蓋方法將永遠也不會影響其他任何方法的行爲。

        你可以消除類中可覆蓋方法的自用特性,而不改變它的行爲。將每個可覆蓋方法的代碼體移到一個私有的“輔助方法”中,並且讓每個可覆蓋的方法調用它的私有輔助方法,然後直接調用私有的輔助方法來代替可覆蓋方法,去除可覆蓋方法的自用調用。

總結:

  • 爲繼承而設計的類,要提供文檔說明,並在發佈前編寫子類進行測試;
  • 對於普通的類,要儘可能地禁止其子類化,可以將其聲明爲final或使用靜態方法來代替構造器;
  • 對於無法禁止子類化的類,要消除這種類中可被子類覆蓋的方法的自用性,即在父類中使用的方法,要確保其在子類中不可被覆蓋。

6. 接口優先於抽象類

使用接口定義類型的好處:

  • 現有的類可以很容易被更新,以實現新的接口類型
  • 接口是定義mixin(混合類型)的理想選擇
  • 接口允許我們構造非層次結構的類型框架

        接口和抽象類兩者最明顯的區別是抽象類允許包含某種方法的實現,但是接口則不允許。 

        雖然接口不允許包含方法的實現,但是,使用接口來定義類型並不妨礙它對於實現上帶來的幫助。通過對每個重要的接口都提供一個骨架實現類,把接口和抽象類的優點結合起來。

        骨架實現類與普通的實現上有個明顯的不同就是它會包含一些簡單實現。雖然它實現了接口,並且是爲了繼承而設計的。但是其中的簡單實現不是抽象的,而是最簡單的可能的有效實現。你可以原封不動的使用,也可以看情況將它子類化。

        下圖是Java容器框架圖,其中就利用了接口和骨架實現類。

        

       

public abstract class AbstractMapEntry<K, V> implements Map.Entry<K, V> {
    public abstract K getKay();
    public abstract V getValue();
    
    public V setValue(V value) {
        throw new UnsupportedOperationException();
    }


    @Override
    public int hashCode() {
        return hashCode(getKay()) ^ hashCode(getValue());
    }
    
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return true;
        }
        if (!(obj instanceof Map.Entry)) {
            return false;
        }
        Map.Entry<?, ?> arg = (Map.Entry)obj;
        return equals(getKey(), arg.getKey()) && equals(getValue(), arg.getValue());
    }
    
    private static boolean equals(Object o1, Object o2) {
        return o1 == null ? o2 == null : o1.equals(o2);
    }
    
    private static int hashCode(Object obj) {
        return obj == null ? 0 : obj.hashCode();
    }
}

        設計公有接口要非常謹慎。接口一旦被公開發行,並且被廣泛實現,再想改變這個接口幾乎是不可能的。一般來說,要想在公有接口中增加方法,並不破壞實現這個接口的所有現有的類,這是不可能的。之前實現該接口的類都將漏掉新增加的方法,並且無法再通過編譯。

7. 接口只用於定義類型

        當類實現接口時,接口就充當了可以引用這個類的實例的類型。類實現了接口,就表明客戶端可以讓這個類的實例執行接口定義的動作。爲了任何其他的目的而定義接口是不恰當的。
        常量接口模式是對接口的不良使用:

public interface PhysicalConstants {
    static final double AVOGANDROS_NUMBER = 6.02214199e23;
    static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
    static final double ELECTRON_MASS = 9.10938188e-31;
}

        類在內部使用某些常量,純粹是實現細節。實現常量接口,會導致把這樣的實現細節泄露到該類的導出API中。更糟糕的是它代表了一種承諾,如果將來的版本中這個類被修改了,不再需要使用這些常量了,它仍必須實現這個接口,以確保二進制兼容性。如果非final類實現了常量接口,它的所有子類的命名空間也會被接口中的常量所“污染”。  

導出常量,通常有三種處理方法:

  • 如果常量與某個現有類或者接口緊密相關,就應該把常量添加到這個類或接口
  • 如果常量被看作枚舉類型的成員,就應該使用枚舉類型
  • 將常量定義在不可實例化的工具類

        工具類通常需要客戶端要用類名來修飾這些常量名,如果大量利用工具類導出的常量,可以通過靜態導入機制(Java 1.5 後引入),避免用類名來修飾常量名。

// Use of static import to avoid qualifying constants
import static com.effectivejava.science.PhysicalConstants.*;

public class Test {
    double atoms(double mols) {
        return AVOGADROS_NUMBER = mols;
    }
    ...
    // Many more uses of PhysicalConstants justify static import
}

8. 類層次優於標籤類

        有時候可能會遇到帶有兩種甚至更多種風格的實例的類,幷包含表示實例風格的標籤域。

class Figure {
    enum Shape {
        RECTANGLE,
        CIRCLE
    }

    final Shape shape;

    double length;
    double width;

    double radius;

    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    double area() {
        switch(shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError();
        }
    }
}

        這種標籤類有許多缺點。它充斥着樣板代碼,將多個實現擠在單個類中,可讀性很差;內存佔用增加;如果要添加風格,必須得給每個條件語句添加一個條件;實例的數據類型沒有提供任何關於其風格的線索。簡而言之,標籤類過於冗長、容易出錯、效率低下

        Java可以採用子類型化來構建類層次,以此替換標籤類。

將標籤類轉變成類層次,具體做法如下:

  • 定義一個抽象類,將標籤類中依賴標籤值的方法定義在抽象類中
  • 爲每種原始標籤類都定義根類的具體子類
abstract class Figure {
    abstract double area();
}

class Circle extends Figure {
    final double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    double area() {
        return Math.PI * (radius * radius);
    }
}

class Rectangle extends Figure {
    final double length;
    final double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    double area() {
        return length * width;
    }
}

        類層次結構避免了標籤類所有的缺點,更重要的是它反映了類型之間本質上的層次關係,有助於增強靈活性,並可提供編譯時檢查,且非常容易擴展。 

9. 用函數對象表示策略

        有些語言支持函數指針、代理、lambda表達式,或者支持類似的機制,允許程序把“調用特殊函數的能力”存儲起來並傳遞這種能力。這種機制通常用於允許函數的調用者通過傳入第二個函數,來指定自己的行爲

        Java沒有提供函數指針,但可以用對象引用實現同樣的功能。可以定義這樣一種對象:它的方法執行其他對象(這些對象被顯式傳遞給這些方法)上的操作。如果一個類僅僅導出一個這樣的方法,它的實例實際上就等同於一個指向該方法的指針。這樣的實例被稱爲函數對象

/*
* StringLengthComparator導出一個帶兩個字符串參數的方法
*指向它對象的引用可以被當作是一個指向該比較器的函數指針,它可以在任意一對字符串上調用
*/
class StringLengthComparator {
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

        上述方法是一個比較器,它根據長度給字符串排序。指向StringLengthComparator對象的引用可以被當作是一個指向該比較器的“函數指針”,可以在任意一個字符串上被調用。換句話說,StringLengthComparator實例是用於字符串比較操作的具體策略。 作爲典型的具體策略類,StringLengthComparator是無狀態的,它沒有域,所以這個類的所有實例在功能上都是相互等價的。因此,它作爲一個Singleton是非常合適的,可以節省不必要的對象創建開銷:

class StringLengthComparator {
    private StringLengthComparator() {}

    public static final StringLengthComparator INSTANCE = new StringLengthComparator();

    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

        爲了把StringLengthComparator實例傳遞給方法,需要適當的參數類型。使用StringLengthComparator並不好,因爲客戶端將無法傳遞任何其他的比較策略。因此,在設計具體的策略類時,需要定義一個策略接口

public interface Comparator<T> {
    public int compare(T t1, T t2);
}

        具體的策略類往往使用匿名類聲明: 

Arrays.sort(stringArray, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
});

        使用匿名類時,將會在每次執行調用時創建一個新的實例。如果它被重複執行,可以考慮將一個函數對象存儲到一個私有的靜態final域裏,並重用它。這樣做另外一個好處是:可以爲這個函數對象取一個有意義的域名稱。

//通過公有靜態final域導出具體實現策略
class Host {
    private static class StrLenCmp implements Comparator<String> {
        public int compare(String s1, String s2) {
            return s1.length() - s2.length();
        }
    }

    public static final Comparator<String> STRING_LENGTH_COMPARATOR = new StrLenCmp();
}

10. 優先考慮靜態成員類

        嵌套類是指被定義在另一個類的內部的類。嵌套類存在的目的應該是爲它的外圍類提供服務。如果嵌套類將來可能會用於其它的某個環境中,它就應該被設計爲頂層類。
        嵌套類有四種:靜態成員類、非靜態成員類、匿名類、局部類

        靜態成員類是最簡單的一種嵌套類可以訪問外部類的所有成員,包括那些聲明爲私有的成員。靜態成員類是外圍類的一個靜態成員,與其他的靜態成員一樣,也遵守同樣的可訪問性規則。如果它被聲明爲私有的,它就只能在外圍類的內部纔可以被訪問。靜態成員類的一種常見用法是作爲公有的輔助類,僅當與它的外部類一起使用時纔有意義。

        非靜態成員類的每個實例都隱含着與外圍類的一個外圍實例相關聯。在非靜態成員類的實例方法內部,可以調用外圍實例上的方法,或者利用修飾過的this構造獲得外圍實例的引用。

        非靜態成員類的一個常見用法是定義一個Adapter,它允許外部類的實例被看作是另一個不相關的類的實例。同樣地,諸如Set和List這種集合接口的實現往往也使用非靜態成員類來實現它們的迭代器(iterator):

//非靜態成員類實現了Set的集合視圖
public class MySet<E> extends AbstractSet<E> {
    ...

    public Iterator<E> iterator() {
        return new MyIterator();
    }

    private class MyIterator implements Iterator<E> {
        ...
    }
}

        匿名類沒有名字,它在使用的同時被聲明和實例化,由於匿名類出現在表達式中,它們必須保持簡短(大約10行或者更少些),否則就會影響程序的可讀性。

匿名類的常見用法:

  • 動態地創建函數對象,如匿名的Comparator實例
  • 創建過程對象,如Runnable、Thread、TimeTask實例
  • 用在靜態工廠方法的內部

        局部類是四種嵌套類中用的最少的類,在任何“可以聲明局部變量”的地方都可以聲明局部類,並且局部類也遵守同樣的作用域規則。它有名字,可被重複地使用,它不能包含靜態成員,必須非常簡短,以免影響可讀性。        

        四種嵌套類都有自己的用途。如果一個嵌套類需要在單個方法之外仍然可見,或者它太長了,不適合放在方法內部,就應該使用成員類。如果成員類的每個實例都需要一個指向其外圍實例的引用,就要把成員類聲明爲非static的;否則就聲明爲static的。如果這個嵌套類屬於一個方法的內部,而你只需要在一個地方創建實例,並且已經有了一個預置的類型可以說明這個類的特徵,就要把它做成匿名類;否則,就做成局部類。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章