Effective Java: 類和接口

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

  1. 儘可能的使每個類或者成員不被外界訪問
  2. 對於頂層類,接口,只有兩種訪問級別: 包級私有(package-private)和公有(public)
  3. 對於成員,有四種訪問級別(private,package-private,protect,public)
  4. 如果一個類只對一個類可見,則應該將其定義爲私有的內部類,而沒必要public的類都應該定義爲package private
  5. 子類的訪問級別不允許低於父類的訪問級別.

小結

  1. 應該始終儘可能的降低可訪問性
  2. 除了公有靜態final域外,公有類都不應該包含非公有域.

14.在共有類中使用訪問方法,而非共有域

示例

  class Point {
    public double x; //
    public double y;
  }
  1. 如果是公有類的時候,應該使用私有成員,並提供setter方法(除非不可變域)
  2. 包級私有,或內部類,直接暴露則沒有本質的錯誤.
class Point {
    private double x;
    private double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    public void setX(double x) {
        this.x = x;
    }

    public void setY(double y) {
        this.y = y;
    }
}

15.使可變性最小

建議

  1. 不提供可以改變本對象狀態的方法
  2. 保證類不會被擴展(防止子類改變)
  3. 使所有的域都是 final
  4. 使所有的域都稱爲private
  5. 確保對於任何可變組件的互斥訪問

示例:

Complex.java

public Complex add(Complex c) {
        return new Complex(re + c.re, im + c.im);
    }

示例中除了標準的 Object 方法之外,運算操作都是創建新的實例,而不改變原來對象,這種做法被稱爲 函數的(functional)做法 .

提示:

  • 不可變對象比較簡單
  • 不可變對象本質上是線程安全的
  • 不僅可以共享對象,甚至可以共享不可變對象的內部信息.[BigInteger內部的內部數組]
  • 不可變對象爲其他對象提供了 大量的構件(building blocks)[map keyset element]

缺點

  1. 不可變對象對於每個不同的值 都需要創建一個單獨的對象.
  2. 由於1的問題.導致多步操作會有性能問題.這種問題的解決辦法就是提供一個公有的可變配套類,如StringStringBuilder.

保持不可變性

爲了保持不被子類化,可以使用final修飾.除此之外,還有一種方法:

  1. 讓類的構造器變成 private/package-private,添加靜態工廠來代替 公有構造器,見[第一條]
  2. 使用靜態工廠方法,具體實現類可以有多個,還能進行object cache[第一條]

小結

  1. 堅決不要爲每個 get 方法編寫一個 對應的 set方法,除非有很好的理由.
  2. 如果類不可做成 不可變的,也該儘量 限制其可變性(final 域)
  3. 當實現Serializable,一定要實現readObject/readResolve方法,或者使用ObjectOutputStream.writeUnshared/ObjectInputStream.readUnshared

案例: TimeTask : 可變,但狀態空間被有意的設計的很小.

16.複合優於繼承

簡介

專爲 繼承設計的,包內部 繼承非常安全,是代碼重用的有效手段.
對於 具體類 進行跨越包邊界的繼承是危險的.

繼承缺點

  • 打破封裝性,子類依賴父類實現細節,父類改變,子類會遭到破壞
    比如: HashSet記錄添加元素個數的方法,addAll內部調用了add,導致程序錯誤.

示例代碼:InstrumentedHashSet.java

  • 在後續升級版本中,如果父類新增了與子類相同簽名,返回值不同的方法,子類無法通過編譯,如果新增了簽名和返回值都相同的方法,則會發生覆蓋

複合方式

  • 新類中增加一個引用現在類私有域,通過轉發實現

示例: ForwardingSet.java

public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;
    public ForwardingSet(Set<E> s) {this.s = s;}

    public boolean add(E e) {return s.add(e);}
}

缺點

  • 不適用於callback frameworks,在回調框架中,需要將自身傳遞給調用對象來回調.
    而被包裝起來的對象並不知道 它被包裝的 外面的對象, 因此導致回調失敗.

繼承和符合選擇問題

  • 繼承應該在is-a的場景中使用
  • 繼承除了會繼承父類的API功能,也會繼承父類的設計缺陷,而組合則可以隱藏成員類的設計缺陷,通過轉發暴露部分API

17.要麼爲繼承而設計,要麼禁用繼承

簡介

  1. 一個類必須在文檔中說明,每個可重寫的方法,在該類的實現中的哪些地方會被調用。調用時機、順序、結果產生的影響,包括多線程、初始化等情況。
  2. 被繼承類應該通過謹慎選擇protected的方法或成員,來提供一些hook,以便能進入到它的內部工作流程中.如:java.util.AbstractList::removeRange
  3. 構造器不能調用可被覆蓋的方法,因爲父類構造函數比子類構造函數先執行,導致子類重寫方法比子類構造器先執行
  4. 如果實現了Serializable/Cloneable,無論是 clone還是readObject都不可以調用可複寫方法
  5. 如果實現了SerializablereadResolve/writeReplace必須是protected,而非private,否則子類會忽略.

總結

  • 爲繼承而設計的類,對這個類會有一些實質性的限制.
  • 不是爲繼承而設計的類,應禁止子類化.

18.接口優於抽象類

簡介

Java 語言提供了兩種機制: 接口和抽象類,
Java是單繼承,多實現的,因此抽象類作爲類型定義受到了很大的限制.

  • 通過接口,現有類可以很容易的被更新
  • 接口是mixin(混合類型)的理想選擇(提供某些可供選擇的行爲)
  • 接口允許我們構建非層次結構的框架(利用接口多繼承特性)
public interface Singer{}
public interface Writer{}
public interface SingerWriter extends Singer ,Writer{}

有效的避免了繼承導致的臃腫的類層次(組合爆炸).

注意事項

  • 對每個重要接口都提供一個抽象的骨架實現類,把接口和抽象類的優點結合起來
  • 通過把對接口的實現轉發到一個擴展了骨架實現的內部私有實例上,稱作模擬多重實現,示例:Map.EntryAbstractMapEntry
  • 骨架類是爲了繼承而設計的[參考第17條規範]
  • 還有一種簡單實現,AbstractMap.SimpleEntry(可能是基本實現,可能是空實現)

區別

  • 抽象類演變比接口方便,可以在後續版本中新增新的方法,並提供默認實現,現有實現類都提供這個新的方法,接口則不行

小結

  • 接口一旦發行,並廣泛實現後,想要改變幾乎不可能.
  • 通過接口定義類型,可以允許多實現(多繼承)
  • 但是演進需求大於靈活性、功能性時,抽象類更合適
  • 提供接口時,提供一個骨架實現,同時審慎考慮接口設計

19.接口只用於定義類型

簡介

接口充當了可以引用這個類的實例類型Type.

不良使用

常量接口: 如:

public interface Constants{
static final int AAA  = 0;
}

這種接口,會使用戶糊塗,並使接口污染,如果這個類被修改了,
意味着,在後續版本中,不再需要這些常量了,它依然必須實現這個接口.

合理方案

  • 只在當前類和接口中導出這些常量.
    如: Integer.MAX_VALUE
  • 不可實例化的工具類中導出常量,配合靜態導入(static import)
  • 接口應該 只被用來定義類型,而不是定義常量.

20.類層次優於標籤類

概述

標籤類,就是在內部定義一個tag變量,由其控制功能的轉換,如下類型

class Figure {
    enum Shape {RECTANGLE, CIRCLE};
    // Tag field - the shape of this figure
    final Shape shape;
}

標籤類過於冗長,容易出錯

解決方案

通過子類化解決.爲每種標籤都定義具體的子類.

示例代碼: Figure

abstract class Figure {}

class Rectangle extends Figure {}
class Circle extends Figure {}

好處:

  • 代碼簡單清除,沒有樣板代碼
  • 所有域都是final的,在初始化構造器時就初始化數據域
  • 多個程序員可以獨立的擴展類層次結構

建議

當編寫包含顯示標籤域的代碼時,應考慮標籤是否可以取消.

21.用函數對象表示策略

簡介

  • 允許程序把調用特殊函數的能力存儲起來並傳遞這種能力.這種機制通常允許函數調用者傳入第二個參數來指定自己的行爲.

  • 定義一種對象,它的方法執行其他對象上的操作,這樣的類被稱爲函數對象.如Compare接口的實現類.

  • 這種對象實例,可以稱爲其他對象具體策略

  • 這種策略類,沒有狀態,沒有域,所有實例都是等價的,因此最好用單例來實現,如:

//第一種
 Arrays.sort(new short[]{}, new Comparator<String>() {
          @Override public int compare(String o1, String o2) {
            return 0;
          }
        });

//第二種
  class StringLengthComparator implements Comparator<String> {
    private StringLengthComparator() {
    }

    public static final StringLengthComparator INSTANCE = new StringLengthComparator();

    @Override public int compare(String o1, String o2) {
      return o1.length() - o2.length();
    }
  }

總結

  • 函數指針的主要途徑是實現策略模式
  • 需要聲明一個接口來表示該策略(如Comparator<T>,一般是泛型接口)
  • 當一個具體的策略只使用一次時,使用匿名內部類
  • 當一個具體策略被重複使用時,使用單例來實現

22.優先考慮靜態成員類

簡介

有四種嵌套類: 靜態成員類,非靜態成員類,匿名類與局部類.除了第一種,其他三種都被稱爲內部類.

靜態成員

  • 靜態成員類是最簡單的一種嵌套類,可以看成是一種恰好被聲明在另一個類的內部的普通類. 和其他靜態成員規則一致.
  • 靜態成員類的一種常見用法是作爲公有的輔助類.僅當與外部類一起使用時纔有意義.
  • 可以在外圍實例之外獨立存在,用於表示(封裝)外部類的一些成員.

非靜態成員

  • 每個實例都隱含着外圍類的一個實例關聯.(內存泄漏一般也是由此引起),持有強引用
  • 不可以在外圍實例之外獨立存在
  • 如果內部類不需要引用外部類的成員和方法,則一定要將其定義爲static,避免空間/時間開銷,避免內存泄漏

匿名類

  • 沒有名字,不是外圍類的成員,在使用的時候被聲明和實例化
  • 有諸多限制,不能instanceof,無法擴展一個類或者接口.
  • 通常像Runnable,Thread在內部類中使用居多

局部類

  1. 局部類是使用最少的類,在任何可以聲明局部變量地方都可以聲明局部變量.
  2. 是否static取決於其定義的上下文
  3. 可以在作用域內重複使用
  4. 不能有static成員

總結

如果成員類每個實例都需要一個指向其外圍實例的引用,用非靜態
否則,用靜態

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