1. 程式人生 > >Effective Java (3rd Editin) 讀書筆記:1 建立和銷燬物件

Effective Java (3rd Editin) 讀書筆記:1 建立和銷燬物件

1 建立和銷燬物件

Item 1:考慮用靜態工廠方法取代構造器

    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }

靜態工廠方法的優點:

  1. 有名字,因此可以直接看出來它的用法,如 Boolean.valueOf(bool)
  2. 不要每次建立新物件
  3. 可以返回方法返回型別的子類
  4. 可以根據輸入引數,返回不同的型別,比如 EnumSet.of(...) 方法,根據元素長度,返回 RegularEnumSet 或者 JumboEnumSet
  5. 方法返回物件的類,在寫此方法時,可以不存在。典型的例子是,JDBC

靜態工廠方法的缺點:

  1. 只提供靜態工廠方法的類,如果沒有 public 或 protected 的構造器,不能有子類
  2. 由於靜態工廠方法在 API 文件中沒有被特別標註(像構造器那樣),程式設計師難以找到它們

Item 2:當構造引數很多時,考慮使用 builder

當構造時的引數不超過 3 個時,通常使用 Telescoping constructor pattern 或者 JavaBean Pattern

// Telescoping constructor pattern
public
HashMap(); public HashMap(int initialCapacity) public HashMap(int initialCapacity, float loadFactor)

Telescoping constructor pattern 的缺點是,當引數多於 3 個時,難以書寫和閱讀。

// JavaBean Pattern
public class User {
    private String name;
    private String password;
    
    public void setName(String name);
    public
void setPassword(String password); }

JavaBean Pattern 的缺點是無法實現類的 immutable,因為每個使用物件的客戶都可以用 set 方法修改物件的屬性。

當構造器或者靜態工廠方法含有超過(或者將來可能超過) 3 個的引數時,推薦使用 buider。

// 支援類繼承拓展的 builder pattern
public abstract class Pizza {
    public enum Topping {HAM, MUSHROOM, ONION, PEPPER, SAUSAGE}
    final Set<Topping> toppings;
    
    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
        public T addTopping(Topping topping) {
            topping.add(Objects.requireNonNull(topping));
            return self();
        }
        
        abstract Pizza build();
        
        // Subclasses must override this method to return "this"
        protected abstract T self();
    }
    
    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone();
    }
}

Item 3:使用私有構造器或者列舉型別來加強單例屬性

// public final 欄位的單例
public class Elvis {
    public static final Elvis INSTANCE = new Elvis();
    private Elvis() {...}
    
    public void leaveTheBuilding() {...}
}

public final 欄位實現單例的優點是:

  1. 顯然是個單例模式
  2. 簡單
// 靜態工廠的單例
public class Elvis {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() {...}
    public static Elvis getInstance() { return INSTANCE; }
    
    public void leaveTheBuilding() {...}
}

靜態工廠實現單例的優點:

  1. 靈活性,比如每個執行緒一個單例
  2. 可以實現泛型單例工廠
  3. 方法的引用可以作為 supplier,如 Elvis::instance 是一個 Supplier<Elvis>
// 列舉實現的單例 -- 推薦方式
public enum Elvis {
    INSTANCE;
    
    public void leaveTheBuilding() {...}
}

列舉型別實現單例可以保證絕對的安全:

  1. 保證單例,即使是面對複雜的序列化或反射攻擊(前兩個方法做不到)
  2. 免費提供序列化機制

Item 4:使用私有構造器來加強不可例項化

抽象類不能保證不可例項化性(noninstantiability),其子類可以例項化,而且它會誤導使用者認為此類被設計用來繼承。

可行的方案是私有化構造器:

// Noninstantiable utility class
public class UtilityClass {
    // Suppress default constructor for noninstantiability
    private UtilityClass() {
        throw new AssertionErrot(); // 防止反射例項化
    }
    ...
}

Item 5:使用依賴注入取代硬編碼的依賴例項化

硬編碼的依賴例項化是意譯,原文是 hardwiring resources。

// 靜態工具模式(不建議,不靈活且不可測試)
public class SpellChecker {
    private static final Lexicon dictionary = ...;
    
    private SpellChecker() {} // Noninstantiable
    
    public static boolean isValid(String word) {...}
    public static List<String> suggestions(String typo) {...}
}

// 單例模式(不建議,不靈活且不可測試)
public class SpellChecker {
    private final Lexicon dictionary = ...;
    
    private SpellChecker() {}
    public static INSTANCE = new SpellChecker(...);
    
    public static boolean isValid(String word) {...}
    public static List<String> suggestions(String typo) {...}
}

在實現一個類時,如果它依賴了其他資源且該資源的型別會影響此類,那麼不要使用單例模式或者靜態工具模式,也不要讓它硬編碼建立依賴例項。推薦的做法是,將依賴物件或依賴物件的工廠作為引數,傳遞給構造器、靜態工廠方法或 builder,這就是依賴注入,它大大增強了類的靈活性、複用性和可測試性

// 依賴注入,具備了靈活性和可測試性
public class SpellChecker {
    private final Lexicon dictionary;
    
    private SpellChecker(Lexicon dictionary) {
        this.dictionary = dictionary;
    }
    
    public static boolean isValid(String word) {...}
    public static List<String> suggestions(String typo) {...}
}

對於大專案,通常包含成千上萬的依賴,使用依賴注入會使得專案複雜化,但是可以通過使用優秀的依賴注入框架來解決這個問題,比如 Dagger、Guice 或 Sping。

Item 6:避免建立不必要的物件

建立了不必要的物件的例子:

  • String s = new String("bikini"); 中建立了兩次 String 物件。
  • String.matches(...) 方法中,新建了 Pattern 物件來匹配,如果多次呼叫此方法,會多次建立 Pattern 物件。
  • Long sum = 0L; sum += 1; 原始型別的自動裝箱也會建立不必要的物件,因此儘量使用原始型別替代其包裝類。

但是,有些時候應該建立重複物件(Item 50)。因為沒有做好必要的防禦性拷貝可能帶來可怕的 bugs 和安全漏洞,而建立了不必要的物件僅僅會影響程式碼風格和效能。

Item 7:消除過時的物件引用

// ArrayList
public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

記憶體洩漏的來源:

  1. 如 ArrayList 中用到的物件陣列,對於這類自己管理記憶體的類,程式設計師要對記憶體洩漏保持警惕
  2. 快取。常常使用 LinkedHashMap 在記憶體緊張時回收最近沒有訪問的物件
  3. 監聽器和回撥。可以使用 WeakHashMap 儲存 callback 的弱引用

Item 8:不要使用 finalizer 和 cleaner

filnalizer 的行為不可預測,危險,通常都不需要。cleaner 不如 finalizer 那麼危險,但是仍然不可預測,緩慢,通常也不需要。最好的建議就是不要使用它們。

Item 9:用 try-with-resources 取代 try-finally

try-with-resources 語句塊簡潔又周到,是 Java 7 以來最優的關閉資源的方式,能使用它的場合就使用它,否則再考慮 try-finally。

舉個例子:

	BufferedReader br = new BufferdReader(new FileReader(path));
	try {
        return br.readLine();
	} finally {
        if (c != null) {
            c.close();
        }
    }

如果物理裝置出現故障無法訪問,readLine() 會丟擲異常,在 finally 程式碼塊中 close() 也會丟擲異常, 第二個異常會泯滅第一個異常,使得使用者看不到自己真正關注的異常。

try (BufferedReader br = new BufferdReader(new FileReader(path))) {
    return br.readLine();
} 

使用 try-with-resources 程式碼塊後,兩個異常都會保留,且 close() (隱式自動關閉)的異常隱含在 readLine() 的異常中,因此保留了使用者真正關心的異常,同時被隱含的異常並沒有被丟棄,可以在 stack trace print 中看到,還可以用 getSuppressed() 方法獲取到 Throwable 物件。