『Effective Java』讀書整理

書地址 :鏈接: https://pan.baidu.com/s/1kUAwYgv 密碼: ij4j

- Chapter 3 適用於所有對象

8. 重寫equals方法

三個原則:對稱性、傳遞性、一致性

9. 重寫equals方法必定要重寫hashCode方法

例如在HashMap中存儲時會調用該方法

10. 始終要重寫toString方法

便於閱讀,使類用起來更加舒適

11. 謹慎的覆蓋clone方法

相當於另一個構造器

12. 考慮實現Comparable接口

用於對象比較、排序(在集合裏sort)


- Chapter 4 類和接口

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

// 錯誤方式,安全漏洞; 
// 當域爲基本類型或不可變對象時安全;
// 當爲可變對象的引用時存在安全漏洞, VALUE雖不可修改但數組裏的對象可被修改
public static final Thing[] VALUE = {....};
// 正確方式
private static final Thing[] PRIVATE_VALUES = {...};
public static final List<Thing> VALUES = Collections.unodifiableList(Arrays.asList(PRIVATE_VALUES));

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

總有類永遠不應該暴露可變域

15. 使可變性最小化

成爲不可變類的5條規則 :

  1. 不要提供任何會修改對象狀態的方法;
  2. 保證類不會被擴展(fina);
  3. 使所有域都是final的;
  4. 使所有域都成爲私有的;
  5. 確保對於任何可變組件的互斥訪問.

16. 複合優先於繼承

當B和A的關係爲”is-a”時,讓B繼承自A;否則B中應包含一個A的實例(複合),而不是擴展A(繼承)。

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

1>. 關於程序文檔有句格言:好的API文檔應該描述一個給定的方法做了什麼工作,而不是描述它如何做到的。

爲了允許繼承,類還必須遵守其他一些約束:

  • 構造器決不能調用可被覆蓋的方法;
  • 無論是clone(Cloneable接口)還是readObject(Serializable接口),都不可調用可覆蓋的方法,不管是直接還是間接的方式。

18. 接口優先於抽象類

抽象類的演變比接口容易;
骨架實現,即接口的簡單實現

19. 接口只用於定義類型

避免常量接口;
接口應該只被用來定義類型

20. 類層次優先於標籤類

標籤類過於冗長、容易出錯,並且效率底下

21. 用函數對象表示策略

比較器:String.CASE_INSENSITIVE_ORDER

22. 優先考慮靜態成員類

嵌套類種類

  1. 靜態成員類;
  2. 非靜態成員類;
  3. 匿名類;
  4. 局部類。

後三種都被稱爲內部類。

如果聲明成員類不要求訪問外圍實例,就要始終把static修飾符放在它的聲明中,使它成爲靜態成員類,而不是非靜態成員類。如果省略了static修飾符,則每個實例都將包含一個額外的指向外圍對象的引用。例如ViewHolder。


- Chapter 5 泛型

23. 請不要在新代碼中使用原生態類型

  • Set : 原生態類型, 脫離了泛型系統;
  • Set<?> : 無限通配符類型,只能包含某種未知對象類型;
  • Set<Object>: 參數化類型,可以包含任何對象類型。
if (o instanceof Set) {
    Set<?> m = (Set<?>) o;
}

原生態類型只是爲了與引入泛型之前的遺留代碼進行兼容和互用而提供的。

術語 示例
參數化類型 List<String>
實際類型參數 String
泛型 List<E>
形式類型參數 E
無限制通配符類型 List<?>
有限制類型參數 <E extends Number>
遞歸類型限制 <T extends Comparable<T>>
有限制通配符類型 List<? extends Number>
泛型方法 static <E>List<E> asList(E[] a)
泛型令牌 String.class

24. 消除非受檢警告

@SuppressWarnings(“unchecked”)要放在一個聲明上,要將禁止非受檢警告範圍縮到最小;每次使用時都要添加一個註釋,說明爲什麼這麼做是安全的。

// 例如ArrayList中的toArray方法, 註解不加在方法上而是單獨聲明一個局部變量
// 爲的就是縮小非受檢警告範圍, 這麼做是值得的.
public <T> T[] toArray(T[] a) {
    if (a.length < size) {
        // This cast is correct because the array we're creating 
        // is of the same type as the one passed in, which is T[].
        @SuppressWarnings("unchecked")
        T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
        return result;
    }
    System.arrayCopy(elements, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

25. 列表優先於數組

  • 禁止創建泛型數組,優先使用集合;
  • 數組是協變且可以具體化的,泛型是不可變的且可以被擦除的。
  • 混合使用時如何得到編譯時錯誤或者警告時,用列表代替數組。

26. 優先考慮泛型

使用泛型比使用需要在客戶端代碼中進行轉換的類型來的更加安全,也更加容易。只要時間允許,就把現有的類型都泛型化。

27. 優先考慮泛型方法

// 遞歸泛型 類型參數
public static <T extends Comparable<T>> T max(List<T> list) {
    Iterator<T> i = list.iterator();
    T result = i.next();
    while (i.hasNext()) {
        T t = i.next();
        if (t.compare(result) > 0)
            result = t;
    }
    return result;
}

28. 利用有限制通配符來提升API的靈活性

爲了獲得最大限度的靈活性,要在表示生產者或者消費者的輸入參數上使用通配符類型。
PECS表示producer-extends,consumer-super
換句話說, 如果參數化類型表示一個T生產者,就使用

// 用Stack示例
public void pushAll(Iterable<? extends E> src) {
    for (E e : src)
        push(e);
}
public void popAll(Collection<? super E> dst) {
    while (!isEmpty()) 
        dst.add(pop());
}

修改過的使用通配符類型的聲明:PECS規則,list生產T實例,T的comparable消費T實例併產生表示順序關係的整值。comparable始終是消費者,因此使用時始終應該是Comparable<? super T>優先於Comparable<T>。對於comparator也一樣,因此使用時始終應該是Comparator<? super T> 優先於Comparator<T>

public static <T extends Comparable<? super T>> T max(List<? extends T> list)  {
    // 這裏做了修改
    Iterator<? extends T> i = list.iterator();
    T result = i.next();
    while (i.hasNext()) {
        T t = i.next();
        if (t.compare(result) > 0)
            result = t;
    }
    return result;
}

29. 優先考慮類型安全的異構容器

public class Favorites {
    private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();

    public <T> void putFavorite(Class<T> type, T instance) {
        if (type == null) 
            throw new NullPointerException("type is null");
        favorites.put(type, instance);
    }

    public <T> T getFavorite(Class<T> type) {
        return type.cast(favorites.get(type));
    }
}

確保永遠不違背它的類型約束條件:

Collections.checkedXXX();

利用Class.asSubclass方法進行轉換:

public <T extends Annotation> T getAnnotation(Class<T> annotationType);
Class<?> typeOne = Class.forName(typeOneInstance);
getAnnotation(typeOne.asSubclass(Annotation.class));

-Chapter 6 枚舉和註解

30. 用enum代替int常量

枚舉類型有一個自動產生valueOf(String)方法,它將常量的名字轉變成常量本身;
枚舉中的switch語句適合於給外部的枚舉類型增加特定於常量的行爲。

public enum Operation {
    PLUS("+") {double apply(double x, double y) {return x + y;} },
    MIMUS("-") {double apply(double x, double y) {return x - y} },
    TIMES("*") {double apply(double x, double y) {return x * y} },
    DIVEDES("/") {double apply(double x, double y) {return x / y} };

    private String symbol;
    public Operation(String sym) {
        this.symbol = sym;
    }
    @Override
    public void toString() {
        return symbol;
    }
    abstract double apply(double x, double y);
}

31. 用實例域代替序數

所有的枚舉都有一個ordinal方法, 它返回每個枚舉常量在類型中的數字位置。永遠不要根據枚舉的序數導出與它關聯的值,而是要將它保存在一個實例中

public enum Ensemble {
    SOLO(1), DUET(2), TRIO(3);

    private int numberOfMusicians;
    public Ensemble(int size) {
        this.numberOfMusicians = size;
    }
    public int numberOfMusician() {
        return numberOfMusicians;
    }
}

32. 用EnumSet代替位域

正是因爲枚舉類型要用在集合Set中, 所有沒有理由用位域來表示它.EnumSet具有簡潔和性能的優勢.

public class Text {
    public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}

    // 所有的Set都可傳入, 但是EnumSet最好
    // 考慮到可能還有其他實現,所以使用Set<Style>而不是EnumSet<Style>
    public void applyStyles(Set<Style> styles){...}
}

// 下面是將EnumSet實例傳遞給applyStyles方法的客戶端代碼。EnumSet提供了豐富的
// 靜態工廠來輕鬆創建集合, 其中一個如下
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));

33. 用EnumMap代替序數索引

Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<Herb.Type, Set<Herb>>(Herb.Type.class);
for (Herb.Type t : Herb.Type.values()) {
    herbsByType.put(t, new HashSet<Herb>);
}
for (Herb b : garden) {
    herbsByType.get(b.type).add(b);
}

34. 用接口模擬可伸縮的枚舉

避免擴展枚舉類型(繼承), 採用用枚舉類型實現接口(實現)

// 定義一個接口
public interface Operation{...}
// ExtendOperation實現了這個接口
public ExtendOperation implements Operation{...}
public static void main(String[] args) {
    double x = 3.3;
    double y = 3.4;
    // 方法一
    test(ExtendOperation.class, x, y);
    // 方法二
    test(Arrays.asList(ExtendOperation.values()), x, y);
}
// 方法一 : 確保Class對象既表示枚舉又表示Operation的子類型
private static <T extends Enum<T> & Operation> void test(
    Class<T> opSet, double x, double y) {
    for (Operation op : opSet.getEnumConstants()) {
        // do sth
    }
}
// 方法二 
private static void test(Collection<? extends Operation> opSet, double x, double y) {
    for (Operation op : opSet) {
        // do sth
    }
}

35. 註解優先於命名模式

// 註解類, 只用在無參數的靜態方法上
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {}

// 測試
Class testClass = Class.forName(agrs[0]);
for (Method m : testClass.getDeclaredMethods()) {
    // 判斷某個方法是否被Test註解標註
    if (m.isAnnotationPresent(Test.class)) {
        try {
            // 可直接執行說明是靜態方法; 傳入null說明無參數
            m.invoke(null);
        } catch(InvocationTargetException ite) {
            // 1. 實例方法
            // 2. 一個或多個參數
            // 3. 不可訪問的方法
        }
    }
}

// 只有拋出異常纔算成功的註解類
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception> value();
}
// 待測試的方法
@ExceptionTest(ArithmeticException.class)
public static void method1() {
    int i = 0;
    i = i / i;
}
@ExceptionTest(ArithmeticException.class)
public static void method2() {
    int[] arr = new int[1];
    int i = arr[3];
}
@ExceptionTest(ArithmeticException.class)
public static void method3() {
    // do nothing
}

// 測試工具類
if (m.isAnnotationPresent(ExceptionTest.class)) {
    try {
        m.invoke(null);
    } catch (InvocationTargetExcetpion ite) {
        // 出現的異常類型
        Throwable exception = ite.getCause();
        // 期待的異常類型
        Class<? extends Exception> ex = m.getAnnotation(ExceptionTest.class).value();
        // 出現的異常與期待的異常時同一種
        if (ex.instanceOf(exception)) {...}
    }
}

// 多種類型異常註解類
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementTarget.METHOD)
public @interface ExceptionsTest {
    Class<? extends Exception>[] value();
}
// 待測試方法註解
@ExceptionsTest({IndexOutOfBoundException.class, ArithmeticException.class})
public static void method4() {
}

36. 堅持使用Override註解

IDE可檢查

37. 用標記接口定義類型

標記接口,類似於Serializable接口,沒有方法,只是一個空接口作爲標記,被標記過的實例可以通過ObjectOutputStream處理。
兩者比較:標記接口和標記註解
- 標記接口定義的類型是由被標記類的實例實現的;標記註解則是沒有定義這樣的類型。這個類型允許你在編譯時捕捉在使用標記註解的情況下要到運行時才能捕捉到的錯誤;
- 標記接口的另一個優點,可以被跟家精確的鎖定;
- 標記註解勝過標記接口的最大優點在於,它可以通過默認的方式添加一個或者多個註解類型元素,給一倍使用過的註解類型添加更多的信息。隨着時間的推移,簡單類型的標記註解可以演變成豐富的標記註解, 標記接口則不能。
- 標記註解的另一個優點在於,它們是更大的註解機制的一部分。因此,標記註解在那些支持註解作爲編程元素之一的框架中同樣具有一致性。

區分使用

  • 應用到任何程序元素(方法,字段等)而不是類或者接口,必須用標記註解;
  • 標記類和接口, 優先使用標記接口;
  • 標記只用於特殊接口的元素,將標記定義爲該接口的一個子接口;
  • 如果以後需要擴展,用標記註解;
  • 當目標是ElementType.TYPE時,多考慮標記接口。

-Chapter 7 方法

38. 檢查參數的有效性

  • 在方法體的開頭檢查參數;
  • 使用斷言assert,失敗時拋出AssertionError;
  • 檢查構造器的參數尤爲重要

39. 必要時進行保護性拷貝

  • 保護性拷貝是在檢查參數有效性之前進行的,並且有效性檢查是針對拷貝之後的對象;
  • 對於參數類型可以被不可信任方子類化的參數,請不要使用clone進行保護性拷貝

40. 謹慎設計方法簽名

  1. 謹慎選擇方法名稱。
  2. 不要過於追求提供便利的方法。
  3. 避免過長的參數列表(小於等於4)。
  4. 對於參數類型,優先使用接口而不是類。
  5. 對於boolean參數, 優先使用兩個元素的枚舉類型。

41. 慎用重載

  • 對於重載方法(overloaded method)的選擇是靜態的,而對於被覆蓋的方法(overridden method)的選擇是動態的。
  • 避免胡亂使用重載機制的安全而保守的策略是,永遠不要導出兩個具有相同參數數目的重載方法。如果方法是可變參數,保守策略是根本不要重載它。

42. 慎用可變參數

43. 返回零長度的數組或集合,而不是null

private final List<Cheese> cheeseInStock = ....;
private final static Cheese[] EMPTY_CHEESE_ARRAY = new Cheese[0];

public Cheese[] getCheese() {
    return cheeseInStock.toArray(EMPTY_CHEESE_ARRAY);
}

// 集合值的方法
public List<Cheese> getCheeseList() {
    if (cheeseOfStock .isEmpty()) {
        return Collections.emptyList();
    } else {
        return new ArrayList<Cheese>(cheeseOfStock);
    }
}

44. 爲所有到處的API元素編寫文檔註釋

-Chapter 8 通用程序設計

45. 將局部變量的作用域最小化

  • 要是局部變量的作用域最小化,最有力的方法就是在第一次使用它的地方聲明。
  • 幾乎每個局部變量的聲明都應該包含一個初始化表達式,如果沒有則應推遲聲明。try-catch例外。
  • 如果循環終止之後不再需要循環變量的內容,for循環優於while循環。
// n的作用是:避免每次循環產生額外計算的開銷
for (int i = 0 , n = getSize(); i < n; i++) {
    doSomething(i);
}

46. for-each循環優於傳統的for循環

三種情況無法使用for-each
1. 過濾:如果需要遍歷集合,並刪除選定的元素,就需要使用顯示的迭代器,以便可以調用它的remove方法。
2. 轉換:如果需要遍歷列表或者數組,並取代它的部分或者全部元素值,就需要列表迭代器或者數組索引,以便設定元素的值。
3. 平行迭代:如果需要並行的遍歷多個集合,就需要顯示的控制迭代器或者索引變量,以便所有迭代器或者索引變量都可以得到同步前移。

47. 瞭解和使用類庫

  • 使用標準類庫而不是專門的實現。
  • Collections Framework
  • java.util.concurrent包含高級併發工具來簡化多線程的編程任務,還包含低級別的併發基本類型

48. 如果需要精確的答案, 請避免使用float和double

使用int或者long或者BigDecimal替代。

49. 基本類型優先於裝箱基本類型

50. 如果其他類型更適合, 則儘量避免使用字符串

  • 字符串不合適代替其他的值類型。
  • 字符串不合適代替枚舉類型。
  • 字符串不適合代替聚集類型。
  • 字符串也不適合代替能力表。

51. 當心字符串連接的性能

使用StringBuilder

52. 通過接口引用對象

如果有合適的接口類型存在,那麼對於參數、返回值、變量和域來說,就都應該使用接口類型進行聲明,如List。
如果沒喲合適的接口存在,完全可以用類而不是接口來引用對象,如值類String、BigInteger

53. 接口優先於反射機制

54. 謹慎地使用本地方法

使用本地方法提高性能的做法不值得提倡

55. 謹慎地進行優化

  • 努力避免那些限制性能的設計決策。
  • 爲獲得更好的性能而對API進行包裝,這是一種非常不好的想法。

56. 遵守普遍接受的命名慣例

-Chapter 9 異常

57. 只針對異常的情況才使用異常

58. 對於可恢復的情況使用受檢異常,對編程錯誤使用運行時異常

59. 避免不必要地使用受檢異常

60. 優先使用標準的異常

61. 拋出與抽象相對應的異常

底層的異常被傳到高層的異常,高層的異常提供訪問方法(Throwable.getCause)來獲得底層的異常

62. 每個方法拋出的異常都要有文檔

63. 在細節消息中包含能捕獲失敗的信息

64. 努力使失敗保持原子性

65. 不要忽略異常

-Chapter 10 併發

66. 同步訪問共享的可變數據

當多個線程共享可變數據的時候,每個讀或者寫數據的線程必須執行同步。

67. 避免過度同步

  • 爲了避免活性失敗和安全性失敗,在一個被同步的方法或者代碼塊中,永遠不要放棄對客戶端的控制。
  • 在同步區域內做盡可能少的工作。
  • 爲了避免死鎖和數據損壞,千萬不要從同步區域內部調用外來方法。

68. executor和task優先於線程

69. 併發工具優先於wait和notify

  • 除非迫不得已,否則應該優先使用ConcurrentHashMap,而不是使用Collections.synchronizedMap或Hashtable。只要用併發Map替代老式的同步Map,就可以極大地提升應用程序的性能。更一般地,應該優先使用併發集合,而不是使用外部的同步集合。
  • 對於間歇式的定時,始終應該優先使用System.nanoTime,而不是System.currentTimeMills,前者更加準確也更加精確,它不受系統的實時始終的調整影響。
  • 使用應該使用wait循環模式來調用wait方法;永遠不要在循環之外調用wait方法。循環會在等待之前和之後測試條件。
private static final ConcurrentMap<String, String> map = ConcurrentHashMap<>();

public static String intern(String s) {
    String result = map.get(s);
    if (result == null) {
        // 應對併發情況
        result = map.putIfAbsent(s, s);
        if (result == null) {
            result = s;
        }
    }
    return result;
}

70. 線程安全性的文檔化

  • “出現synchronized關鍵字就足以用文檔說明線程安全性”的這種說法隱含了一個錯誤的觀念,即認爲線程安全性是一種“要麼全有要麼全無”的屬性。

線程安全性的幾種級別:

  1. 不可變的(immutable):這個類的實例是不變的。所以,不需要外部的同步。這樣的例子包括String、Long和BigInteger。
  2. 無條件的線程安全(unconditionally thread-safe):這個類的實例是可變的,但是這個類有着足夠的內部同步,所以,它的實例可以被併發使用,無需任何外部同步。其例子包括Random和ConcurrentHashMap。
  3. 有條件的線程安全(conditionally thread-safe):除了有些方法爲進行安全的併發使用而需要外部同步之外,這種線程安全級別與無條件的線程安全相同。這樣的例子包括Collections.synchronized包裝返回的集合,它們的迭代器(iterator)要求外部同步。
  4. 非線程安全(not thread-safe):這個類的實例是可變的。爲了併發地使用它們,客戶必須利用自己選擇的外部同步包圍每個方法調用(或者調用序列)。這樣的例子包括通用的集合實現,例如ArrayList和HashMap。
  5. 線程對立的(thread-hostile):這個類不能安全地被多個線程併發使用,即使所有的方法調用都被外部同步包圍。線程對立的根源通常在於,沒有同步地修改靜態數據。沒有人會有意編寫一個線程對立的類;這種類是因爲沒有考慮到併發性兒產生的後果。幸運的是,在Java平臺類庫中,線程對立的類或者方法非常少。System.runFinalizersOnExit方法是線程對立的,但已經被廢除了。

71. 慎用延遲初始化

  • 在大多數情況下,正常初始化要優先於延遲初始化。如果域只在類的實例部分被訪問,並且初始化這個域的開銷很高,可能就值得進行延遲初始化。
// 正常初始化
private final FieldType field = computeFieldValue();

// 延遲初始化,要使用同步訪問方法
private FieldType field;
synchronized FieldType getField() {
    if (field == null)
        field = computeFieldValue();
    return field;
}
  • 如果出於性能的考慮而需要對靜態域使用延遲初始化,就是用lazy initialization holder class模式。這種模式保證了類要到用到的時候纔會被初始化。
private static class FieldHolder {
    static final FieldType field = computeFieldValue();
}
static Field getField() {
    return FieldHolder.field;
}
  • 如果處於性能考慮而需要對實例域使用延遲初始化,就使用雙重檢查模式。這種模式避免了在域被初始化之後訪問這個域時的鎖定開銷。
private volatile FieldType field;
FieldType getField() {
    // 局部變量result的作用是確保field只在已經被初始化的情況下讀取一次,提升性能
    FieldType result = field;
    if (result == null) {
        synchronized(this) {
            result = field;
            if (result == null)
                field = result = computeFieldValue();
        }
    }
    return result;
}
  • 延遲初始化一個可以接受重複初始化的實例域,可使用單重檢查模式。
private volatile FieldType field;
private FieldType getField() {
    FieldType result = field;
    if (result == null) 
        field = result = computeFieldValue();
    return result;
}

72. 不要依賴於線程調度器

不要讓程序的正確性依賴於線程調度器,否則結果得到的應用將既不健壯也不具有可移植性。作爲推論,不要依賴Thread.yield或者線程優先級。

73. 避免使用線程組

-Chapter 11 序列化

74. 謹慎地實現Serializable接口

爲了繼承而設計的類應該儘可能少地去實現Serializable接口,用戶的接口也應該儘可能少地繼承Serializable接口。
如果一個類或者一個接口存在的目的主要是爲了參與到某個框架中,該框架要求所有的參與者必須實現Serializable接口,這個時候實現或者擴展Serializable接口就很有意義。
內部類不應該實現Serializable,內部類的默認序列化形式是定義不清楚的,然而靜態成員類卻可以實現Serializable。

public class Foo extends AbstractFoo implements Serializable {
    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        // Manually deserialize and initialize superclass state
        int x = s.readInt();
        int y = s.readInt();
        initialize(x, y);
    }

    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        // Manually serialize superclass state
        s.writeInt(getX());
        s.writeInt(getY());
    }

    public Foo(int x, int y) {
        super(x, y);
    }

    private static final long serialVersionUID = 185683560954L;
}

75. 考慮使用自動以序列化形式

public final class StringList implements Serializable {
    private int size = 0;
    private Entry head = null;
    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }
    // ...Remainder omitted
}

當一個對象的物理表示法與它的邏輯數據內容有實質性的區別時,使用默認序列化形式會有以下4個缺點:

  1. 它使這個類的導出API永遠地束縛在該類的內部表示法上。在上面的例子中,私有的StringList.Entry類變成了公有API的一部分。如果在將來額版本中,內部表示法發生了變化,StringList類仍將需要接受鏈表形式的輸入,併產生鏈表形式的輸出。這個類永遠也擺脫不了維護鏈表項所需要的所有代碼,即使它不再使用鏈表作爲內部結構了,也仍然需要這些代碼。
  2. 它會消耗過多的空間。在上面的例子中,序列化形式既表示了鏈表中的每個項,也表示了所有的鏈接關係,這是不必要的。這些鏈表項以及鏈表只不過是實現細節,不值得記錄在序列化形式中。因爲這樣的序列化形式過於龐大,所以把它寫到硬盤中,或者在網絡上發送都將非常慢。
  3. 它會消耗過多的時間。序列化邏輯並不瞭解對象圖的拓補關係,所以它必須要經過一個昂貴的圖遍歷(traversal)過程。在上面的例子中,沿着next引用進行遍歷是非常簡單的。
  4. 它會引起棧溢出。默認的序列化過程要對對象圖執行一次遞歸遍歷,即使對於中等規模的對象圖,這樣的操作也可能引起棧溢出。到底多少個元素會引發棧溢出,這要取決於JVM的具體實現以及Java啓動時的命令行參數,(比如Heap Size的-Xms與-Xmx的值)有些實現可能根本不存在這樣的問題。

修訂版本,transient修飾符表明這個實例域將從一個類的默認序列化形式中省略掉。

public final class StringList implements Serializable {
    private transient int size = 0;
    private transient Entry head = null;
    private static class Entry {
        String data;
        Entry next;
        Entry previous;
    }
    // 添加指定的string到這個集合
    public final void add(String s) {...}

    // 重寫writeObject方法, 與物理表示法的細節脫離
    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeInt(size);
        for (Entry e = head; e != null; e = e.next) {
            s.writeObject(e.data);
        }
    }

    // 重寫readObject方法,與write對應
    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        int numElements = s.readInt();
        for (int i = 0; i < numElements; i++) {
            add((String) s.readObject());
        }
    }
    ...// Remainder omitted
}

儘管StringList的所有域都是瞬時的(transient),但wirteObject方法的首要任務仍是調用defaultWriteObject,readObject方法的首要任務則是調用defaultReadObject。如果所有的實例域都是瞬時的,從技術角度而言,不調用defaultWriteObject和defaultReadObject也是允許的,但是不推薦這樣做。即使所有的實例域都是transient的,調用defaultWriteObject也會影響該類的序列化形式,從而極大地增強靈活性。這樣得到的序列化形式允許在以後的發行版中增加非transient實例域,並且還能保持向前或者向後兼容性。如果某一個實例將在未來的版本中被序列化,然後在前一個版本中被反序列化,那麼,後增加的域將被忽略掉。如果舊版本的readObject方法沒有調用defaultReadObject,反序列化過程將失敗,引發StreamCorrupted Exception異常。
無論是否使用默認的序列化形式,當defaultWriteObject方法被調用的時候,每一個未被標記爲transient的實例域都會被序列化。因此每一個可以被標記爲transient的實例域都應該做上這樣的標記。這包括那些冗餘的域,即它們的值可以根據其他“基本數據類型”計算而得到的域,比如緩存起來的散列值。在將一個域做成非transient的之前,請一定要確信它的值是該對象邏輯狀態的一部分。如果你正在使用一種自定義的序列化形式,大多數實例域,或者所有的實例域則都應該被標記爲transient,就像上面例子中的StringList那樣。
如果正在使用默認的序列化形式, 並且把一個或者多個域標記爲transient,則要記住,當一個實例被反序列化的時候,這些域將被初始化爲它們的默認值。
無論是否使用默認的序列化形式,如果在讀取整個對象狀態的任何其他方法上強制任何同步,則必須在對象序列化上強制這種同步。
不管選擇了哪種序列化形式,都要爲自己編寫的每個可序列化的類聲明一個顯示的序列化版本UID(serial version UID)。第一避免不兼容,第二減小額外計算的開銷。

pirvate static final long serialVersionUID = randomLongValue;

在編寫新類時,爲randomLongValue選擇什麼值並不重要。通過在該類上運行serialver工具,就可以得到這樣一個值,但是,憑空編造一個數值也是可以的。如果想修改一個沒有序列版本UID的現有的類,並希望新的版本能夠接受現有的序列化實例,就必須使用serialver工具爲舊版本生成值。

76. 保護性地編寫readObject方法

當一個對象被反序列化的時候,對於客戶端不應該擁有的對象引用,如果哪個域包含了這樣的對象引用,就必須要做保護性拷貝,這是非常重要的。

指導方針:

  • 對於對象引用域必須保持爲私有的類,要保護性的拷貝這些域中的每個對象。不可變類的可變組件就屬於這一類別。
  • 對於任何約束條件,如果檢查失敗,則拋出一個InvalidObjectException異常。這些檢查動作應該跟在所有的保護性拷貝之後。
  • 如果整個對象圖在被反序列化之後必須進行驗證,就應該使用ObjectInputValidation接口。
  • 無論是直接方式還是間接方式,都不要調用類中任何可被覆蓋的方法。

77. 對於實例控制,枚舉類型優先於readResolve

readResolve特性允許你用readObject創建的實例代替另一個實例。對於一個正在被反序列化的對象,如果它的類定義了一個readResolve方法,並且具備正確的聲明,那麼在反序列化之後,新建對象上的readResolve方法就會被調用。然後該方法返回的對象引用將被返回,取代新建的對象。在這個特性的絕大多數用法中,指向新建對象的引用不需要再被保留,因此立即成爲垃圾回收的對象。

總而言之,應該儘可能地使用枚舉dang來實施實例控制的約束條件。如果做不到,同時又需要一個既可序列化又是實例受控的類,就必須提供一個readResolve方法,並確保該類的所有實例域都爲基本類型,或者時transient。

78. 考慮用序列化代理代替序列化實例

序列化代理模式相當簡單:

  1. 爲可序列化的類設計一個私有的靜態嵌套類,精確地表示外圍類的實例的邏輯狀態。這個嵌套類被稱作序列化代理,它應該有一個單獨的構造器,其參數類型就是那個外圍類。這個構造器只從它的參數中複製數據:它不需要進行任何一致性檢查或者保護性拷貝。從設計的角度來看,序列化代理的默認序列化形式是外圍類最好的序列化形式。外圍類及其序列代理都必須聲明實現Serializable接口。

    private static class SerializationProxy implements Serializable {
        private final Date start;
        private final Date end;
    
        SerializationProxy(Period p) {
            this.start = p.start;
            this.end = p.end;
        }
        private static final long serialVerionUID = 302480420480234L;
    }
  2. 接下來,將下面的writeReplace方法添加到外圍類中。通過序列化代理,這個方法可以被逐字複製到任何類中:

    private Object writeReplace() {
        return new SerializationProxy(this);
    }

    這個方法的存在導致序列化系統產生一個SerializationProxy實例,代替外圍類的實例。換句話說,writeReplace方法在序列化之前,將外圍類的實例轉變成了它的序列化代理。所以序列化系統永遠不會產生外圍類的序列化實例,爲了避免攻擊者僞造,只要在外圍類中添加這個readObject方法即可:

    private void readObject(ObjectInputStream s) throws InvalidationException {
        throw new InvalidationException("Proxy required");
    }
  3. 最後,在SerializationProxy類中提供一個readResolve方法,它返回一個邏輯上相當於外圍類的實例。這個方法使序列化系統在反序列化時將序列化代理轉變回外圍類的實例。
    這個readResolve方法僅僅利用它的公有API創建外圍類的一個實例,這正是該模式的魅力之所在。它極大地消除了序列化機制中語言本身之外的特徵,因爲反序列化實例是利用與任何其他實例相同的構造器、靜態工廠和方法而創建的。這樣就不必單獨確保被反序列化的實例一定要遵守類的約束條件。如果該類的靜態工廠或者構造器建立了這些約束條件,並且它的實例方法在維持着這些約束條件,你就可以確信序列化也會維持這些約束條件。
    上述Period.SerializationProxy的readResolve方法:

    private Object readResolve() {
        return new Period(start, end);
    }

  • 兩個侷限性:它不能與可以被客戶端擴展的類兼容,它也不能與對象圖中包含循環的某些類兼容:如果企圖從一個對象的序列化代理的readResolve方法內部調用這個對象的方法,就會得到一個ClassCastException異常,因爲還沒有這個對象,只有它的序列化代理。
  • 代價:比保護性拷貝進行的開銷大。
  • 當必須在一個不能被客戶端擴展的類(final)上編寫readObject或者writeObject方法的時候,就應該考慮使用序列化代理模式。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章