Effective Java (3rd Editin) 讀書筆記:1 建立和銷燬物件
1 建立和銷燬物件
Item 1:考慮用靜態工廠方法取代構造器
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
靜態工廠方法的優點:
- 有名字,因此可以直接看出來它的用法,如
Boolean.valueOf(bool)
- 不要每次建立新物件
- 可以返回方法返回型別的子類
- 可以根據輸入引數,返回不同的型別,比如
EnumSet.of(...)
方法,根據元素長度,返回RegularEnumSet
或者JumboEnumSet
- 方法返回物件的類,在寫此方法時,可以不存在。典型的例子是,JDBC
靜態工廠方法的缺點:
- 只提供靜態工廠方法的類,如果沒有 public 或 protected 的構造器,不能有子類
- 由於靜態工廠方法在 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 欄位實現單例的優點是:
- 顯然是個單例模式
- 簡單
// 靜態工廠的單例
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {...}
public static Elvis getInstance() { return INSTANCE; }
public void leaveTheBuilding() {...}
}
靜態工廠實現單例的優點:
- 靈活性,比如每個執行緒一個單例
- 可以實現泛型單例工廠
- 方法的引用可以作為 supplier,如
Elvis::instance
是一個Supplier<Elvis>
// 列舉實現的單例 -- 推薦方式
public enum Elvis {
INSTANCE;
public void leaveTheBuilding() {...}
}
列舉型別實現單例可以保證絕對的安全:
- 保證單例,即使是面對複雜的序列化或反射攻擊(前兩個方法做不到)
- 免費提供序列化機制
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;
}
記憶體洩漏的來源:
- 如 ArrayList 中用到的物件陣列,對於這類自己管理記憶體的類,程式設計師要對記憶體洩漏保持警惕
- 快取。常常使用 LinkedHashMap 在記憶體緊張時回收最近沒有訪問的物件
- 監聽器和回撥。可以使用 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 物件。