《Effective Java》讀書筆記

建立和銷燬對象

靜態工廠模式

  • 構造器裏未傳參的成員不會被初始化。int類型是0,布爾類型是false,String類型是null,List<>也是null。

重疊構造器

  • 進階1:javabean模式,使用set方法來初始化成員,缺點是構造過程當中javabean可能處於不一致狀態(能夠理解成該模式下成員的設置的分步進行的,可能某處使用到該類的某個成員時其還未被初始化),而且該模式阻止了把類變成不可能的可能,須要考慮線程安全。
  • 進階2: Builder模式:類裏定義一個靜態類builder(其實就是javabean),對builder初始化完成後使用build()返回該類,Buidler模式的狀態不一致是builder,而不是類自己,而且類自身的成員也可設置成final。

修飾符

  • 長度非零的數組老是可變的,即便是final類型:
public static final int[] VALUES={...} //錯誤
//正確1:增長一個公有的不可變列表
private static final int[] VALUES=...
public static final List< intergeR > VALUES=
{Collections.unmodifiableList(Arrays.adList(PRIVATE_VALUES));
//正確2:返回私有數組的拷貝
private static final int[] VALUES=...
public static final int[] values() {
    return VALUES.clone();
}
複製代碼
  • 若是是公有類,直接暴露數據會有很大的隱患,由於當你未來想改變其內部表示法時已經不可能了,由於共有類的客戶端代碼已經遍及各處了。
public class Point{ //錯誤
    public int x;
    public int y;
}

public class Point{ //正確
    private int x;
    private int y;
    
    public int getX() { return x;}
}
複製代碼

類和接口

使可變性最小化

  • 線程安全最容易的作法:只提供訪問方法,不提供設值方法,對對象的加減乘除都從新返回一個新的對象。對象不會變化,也就不要求同步。
  • 能夠把開銷昂貴的計算結果緩存起來,例如String的hashcode方法,第一次計算後會將結果保存在成員hashCode裏。

複合優先繼承

  • 子類脆弱:例如一個類繼承HashSet,若是子類裏重寫了addAll和add方法來計數,就會致使錯誤,由於HashSet的addAll是基於add方法實現的。不能保證父類不隨着版本而變化,所以extends 子類繼承父類是很是脆弱的。
  • 只有當子類真正是超類的子類型,即A和B,二者確實存在B is A的關係時,類B才應該擴展A,若是答案是否認的,一般狀況下B應該包含A的一個私有實例,而且暴露一個較小的,簡單的API:A本質上不是B的一部分,只是它的實現細節而已。

裝飾者模式(Decorator模式)

結合上面說到的,HashSet是implement Set類的,在HashSet裏重寫了Set接口定義的add,addAll等方法。所以新的子類繼承Hashset重寫add、addAll就不可避免會將HashSet裏的實現繼承下來。html

使用裝飾者模式:ForwardingSet implements Set,該類有成員private final Set s s,構造器裏就是傳入一個Set ,該類不具體實現Set的任何方法,例如:java

public boolean add(E e) {
	return s.add(e);
}
複製代碼

InstrumentedSet extends ForwardingSet,構造器super父類便可,在這個類裏添加一些功能,例如:android

@Override
public boolean add(E e){
    count++;
    return super.add(e);
}
複製代碼

這種模式下,InstrumentedSet 只是一個包裝類,只是對其成員Set進行修飾,爲它增長計數特性。包裝類並不實現具體功能,構造器裏傳入的就是實現具體功能的Set,能夠是HaseSet或者本身實現的Set。程序員

另可參考閱讀: Android源碼學習之裝飾模式應用編程

繼承後構造方法的調用

  1. 若是子類沒有定義構造方法,則調用父類的無參數的構造方法。
  2. 若是子類定義了構造方法,不管是無參數仍是帶參數,在建立子類的對象的時候,首先執行父類無參數的構造方法,而後執行本身的構造方法。
  3. 若是子類調用父類帶參數的構造方法,能夠經過super(參數)調用所須要的父類的構造方法,切該語句作爲子類構造方法中的第一條語句。
  4. 若是某個構造方法調用類中的其餘的構造方法,則能夠用this(參數),切該語句放在構造方法的第一條。 說白了:原則就是,先調用父親的。(沒有就默認調,有了就按有的調,反正只要有一個就能夠了)
public class Son extends Father {
	public Son() {
	//        super(); //沒加默認調用父類無參構造方法
	    super("from son");
	    Log.e("zyz", "son-constructor");
	}

    public Son(String str) {
//        super(); //沒加默認調用父類無參構造方法
        Log.e("zyz", str + " son-constructor-with-params");
    }

    @Override
    public void print() {
        Log.e("zyz", "son-print");
    }
}

public class Son extends Father {
    public Son() {
//        super(); //沒加默認調用父類無參構造方法
        super("from son");
        Log.e("zyz", "son-constructor");
    }

    public Son(String str) {
//        super(); //沒加默認調用父類無參構造方法
        Log.e("zyz", str + " son-constructor-with-params");
    }

    @Override
    public void print() {
        Log.e("zyz", "son-print");
    }
}
複製代碼

接口優於抽象類

抽象類能夠寫實例方法,經過派生繼承,實現代碼複用(子類可直接調用父類方法),但因爲重用方法增長了耦合度,接口的方法必定須要重寫,最大程度實現瞭解耦。數組

類層次優於標籤類

標籤類: 例如使用枚舉或常量定義了圓和矩形,成員裏有半徑、長、寬。在公共方法 計算面積裏,使用switch來判斷是那種形狀,再分別計算。相似的把多個實現亂七八糟地擠在單個類中,破壞可讀性,又增長了內存佔用,由於實例承擔着屬於其餘類型的域。緩存

應該使用類層次來優化: 定義一個抽象類,包含抽象方法:將共有的方法(計算面積),若是有公有的成員還能夠將其放在抽象類中。以後不一樣的類圓和矩形繼承公共抽象類,另外添加本身的參數,並重寫本身的計算面積的方法。安全

優先考慮靜態成員

若是成員類不要求訪問外圍實例,就要定義成靜態內部類。非靜態內部類始終要保持外圍對象的引用,不只消耗內存,還將致使外圍實例沒法被垃圾回收。 例如Map實現的內部都有Entry對象,每一個Entry都與Map關聯,可是entry的方法(getKey/getValue)等並不須要訪問Map,所以私有的靜態成員類是最佳的選擇。bash

  • 若是一個嵌套類須要在單個方法以外可見,或者它太長了不適合放在方法內部,就使用成員類。
  • 若是成員類的每一個實例都須要一個指向外圍實例的應用,就使用非靜態成員類。不然就使用靜態成員類。
  • 若是嵌套類屬於一個方法的內部,且你只須要在一個地方建立實例,而且已經有了一個預置的類型能夠說明這個類的特徵,就使用匿名類。不然就使用局部類。

泛型

列表優先於數組

兩者的不一樣點:數據結構

數組是協變的(covariant)

若是B是A的子類,那麼B[]就是A[]的子類型。

//編譯時不報錯,運行時報錯ArrayStoreException
Object[] test = new Long[1];
test[0] = "test";
複製代碼

而兩個不一樣的類型A、B,List既不是List的子類也不是超類。

List<Object> test2 = new ArrayList<Long>(); //編譯時報錯
test2.add("123");
複製代碼

數組是具體化的(reified)

數組在運行時才知道並檢查他們的元素類型約束。泛型則是經過擦除(erasure)來實現的。泛型只在編譯時強化類型信息,在運行時擦除元素類型信息。擦除就是使泛型能夠與沒有使用泛型的代碼隨意互用。

利用有限制通配符提高API的靈活性

PECE producer-extends,consumer-siper 若是參數化類型表示生產者T,就使用<? extends T>,若是表示消費者T,就使用<? super T>

//src產生E實例供使用,是生產者
public void pushAll(Iterable<? extands E> src) {
    for (E e : src) push(e);
}
//dst消費E實例,是消費者
public void popAll(Collection<E> dst) {
    while(!isEmpty()) {
        dst.add(pop());
    }
}
複製代碼

不要用通配符類型做爲返回參數

枚舉和註解

用enum代替int常量

(android不推薦使用enum)

  • 枚舉本質上是int值
  • 枚舉容許添加任意的方法和域
public enum Test {
    APPLE("test1", 2),
    pen("test2", 1);

    private final String name;

    private final int num;

    Test(String name, int num) {
        this.name = name;
        this.num = num;
    }

    public void print() {
        Log.e("zyz", APPLE.name + APPLE.num);
    }
}
//遍歷枚舉
Test[] values = Test.values();
複製代碼

用實例域代替序數

  • 全部枚舉都有一個ordinal方法,返回每一個枚舉常量在類型中的數字位置。避免使用ordinal方法,除非是編寫EnumSet和EnumMap這種基於枚舉的通用數據結構。使用實例域(相似成員變量)來保存與枚舉相關的值。

註解

  • 註解類型聲明
@Retention(RetentionPolicy.RUNTIME) //運行時保留
@Target(ElementType.METHOD) //只在方法聲明中才是合適的
public @interface MyTest {
    
}
複製代碼

堅持使用Override註解

覆蓋equals時的參數是Object類型的,不然則變成了重載。但若是使用@Override註解後寫錯了編譯器就會報錯。

用標記接口定義類型

  • 標記接口是沒有包含方法聲明的接口,只是指名了某個類實現了具備某種屬性的接口(例如Serializable接口)
  • 標記接口賽過標記註解的兩點:
    1. 接口定義的類型是由被標記類的實例實現的,註解則沒有定義這樣的類型。這個類型容許你在編譯時捕捉到錯誤,而不像註解須要在運行時才能捕捉到
    2. 接口能夠被更加精確地鎖定。假設一個標記只適用於特殊接口的實現,若是定義成標記接口就能夠用它將惟一的接口擴展成它適用的接口。
  • 註解賽過接口的兩點:
    1. 註解能夠不斷演變。而接口一般不可能在實現後再給它添加方法。
    2. 註解是註解機制的一部分。註解能夠做爲支持註解做爲編程元素之一的框架中具備一致性。
  • 接口和註解使用場景:
    1. 若是標記是應用到任何程序元素而不是類或接口,就必須使用註解,由於只有類和接口能夠用來實現或擴展接口。
    2. 若是標記只給類和接口,若要編寫多個只接受有這種標記的方法則優先使用接口,這樣能夠在編譯時進行類型檢查。
    3. 若是要永遠限制這個標記只用於特殊接口的元素,最好將標記定義成該接口的一個子接口。
    4. 若是2,3都是否認的,則應該使用註解。

方法

檢查參數的有效性

  • assert 對於有些參數,方法自己沒有用到,卻被保存起來供之後使用,可使用斷言檢驗這類參數的有效性。若是斷言失敗,則會拋AssertionError。

必要時進行保護性拷貝

慎用重載

  • 類型仍是父類,雖然調用父類方法指向子類引用。

  • 安全而保守的策略是:永遠不要導出兩個具備相同參數數目的重載方法。若是方法使用可變參數,保守的策略是根本不要重載它。

慎用可變參數

  • 若是客戶端調用這個方法時並無傳遞參數進去,它就會在運行時而不是編譯時失敗。

    //帶兩個參數,避免沒有傳參致使的問題
    static init min(int firstArg, int... remainingArgs) {
        int min = firstArg;
        for(int arg : remainingArgs) {
            ...
        }
    }
    複製代碼
  • 在重視性能的狀況下,使用可變參數要特別小型,可變參數方法的每次調用都會致使進行一次數組分配和初始化。可使用多個重載方法,每一個重載方法帶有0至3個普通參數,當參數數目超過3個時,就使用可變參數方法。

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

通用程序設計

for each循環優於傳統的for循環

  • 若是你在編寫的類型是一組元素,實現Iterable能夠容許用戶利用for-each循環遍歷你的類型。
  • 三種常見的沒法使用for-each的狀況:
    1. 過濾——須要遍歷集合並刪除選定元素
    2. 轉換——須要遍歷集合並取代它的部分或所有元素值
    3. 平行迭代——須要並行地遍歷多個集合,就須要顯式地控制迭代器或者索引變量以便全部迭代器或索引變量均可以獲得同步前移

瞭解和使用類庫

  • 僞隨機數生成器

    //錯誤
    Math.abs(new Random().nextInt());
    //正確
    Random.nextInt(int)
    複製代碼
  • 瞭解和使用標準類庫提供的便利工具,而不用浪費時間爲那些與工做不太相關的問題提供特別的解決方案。標準類庫太龐大了,以致於不可能去學習全部文檔,可是每一個程序員都應該熟悉java.lang,java.util,某種程度上還有java.io種的內容。有兩種工具值得特別一提。

    • Collections Framework 用來表示和操做集合
    • java.util.concurrentbao'zhong包中增長了一組併發使用工具

    總而言之,不要從新發明輪子,若是你要作的事情看起來是十分常見的,有可能類庫中已經有某個類完成了這樣的工做。

若是須要精確的答案,請避免使用float和double

float和double類型尤爲不適合用於貨幣計算,由於要讓一個float或double精確地表示0.1(或者10的ren'he'qi'ta任何其餘負數次方值)是不可能的。

改進

使用BigDecimal代替double:

BigDecimal bigDecimal = new BigDecimal(0.1); 
複製代碼

BigDecimal容許你徹底控制舍入,每當一個操做設計舍入的時候,它容許你從8種舍入模式中選擇其一。可是缺點是與基本運算類型比,不只不方便,並且很慢。若是性能很是關鍵,而且又不介意本身記錄是金子小數點,並且涉及的數值又不太大,就可使用int或long(例如0.1改變單位計做10)。若是數值範圍沒超過9位十進制數字,就可使用int。若是不超過18位數值,就可使用long。若是數值超過18位數字,就必須使用BigDecimal。

基本類型優於裝箱基本類型

當程序裝箱了基本類型值時,會致使高開銷和沒必要要的對象建立。

小心字符串鏈接的性能

鏈接操做符不適合運用在大規模的場景中,爲鏈接n個字符串而重複地使用字符串鏈接操做符,須要n的平方級的時間。這是因爲字符串不可變,當兩個字符串被鏈接在一塊兒時,它們的內容都要被拷貝。

使用StringBuilder:

StringBuilder test = new StringBuilder("test");
test.append("test2")
複製代碼

經過接口引用對象

若是有合適的接口類型存在,那麼對於參數、返回值、變量和域來講,就都應該使用接口類型進行聲明。只有當你利用構造器建立某個對象的時候,才真正須要引用這個對象的類。

List<String> list = new ArrayList<>();
複製代碼

這樣會使程序更靈活,當你決定更換實現時,只須要改變構造器中類的名稱:

List<String> list = new Vector<>();
複製代碼

全部的代碼均可以繼續工做,代碼並不知道原來的實現類型,因此對於這種變化並不在乎。

接口優先於反射機制

反射機制容許一個類使用另外一個類,即便當前者被編譯的時候後者還根本不存在,然而這種能力也是要付出代價的:

  • 喪失了編譯時類型檢查的好處(包括異常檢查)
  • 執行反射訪問所須要的代碼很是笨拙和冗長
  • 性能損失

異常

只針對異常的狀況才使用異常

//Dont't do this try { int i = 0; while (true) { range[i++].climb(); } } catch (ArrayIndexOutOfBoundsException e) { } 複製代碼

不要優先使用基於異常的模式:

  • 異常機制的設計初衷是用於不正常的情形,因此不多會有JVM實現試圖對它們進行優化。
  • 代碼塊放在try-catch塊中反而阻止了現代JVM實現原本可能要執行的某些特定優化。
  • 對數組進行比那裏的標準模式並不會致使冗餘的檢查,有些現代的JVM實現會將它們優化掉。
  • 基於異常的循環模式不只模糊了代碼的意圖,還下降了性能,並且它還不能保證正常工做,若是出現不想關的bug,這個模式會悄悄地失效。

努力使失敗保持原子性

通常而言,失敗的方法調用應該使對象保持在被調用以前的狀態。具備這種屬性的方法被稱爲具備失敗原子性(failure atomic)。有幾種途徑能夠實現這種效果:

  1. 在執行操做前檢查參數的有效性,這可使在對象狀態被修改前先拋出適當的異常。
  2. 調整計算處理過程的順序,使得任何可能會失敗的計算部分都在對象狀態被修以前發生。
  3. 編寫一段恢復代碼,由它來攔截操做過程發生的失敗,以及使對象回滾到操做開始以前的狀態,這種辦法主要用於永久性的數據結構。
  4. 在對象的一份臨時拷貝上執行操做,操做完成以後再用臨時拷貝中的結果代替對象的內容。

不要忽略異常

忽略一個異常很是容易,只需將方法調用經過try語句包圍起來,幷包含一個空的catch塊。空的catch塊會使異常達不到應有的目的,至少,catch塊也應該包含一條說明,解釋爲何能夠忽略這個異常。

併發

正確地使用同步能夠保證沒有任何方法會看到對象處於不一致的狀態中。它還能夠保證剛進入同步方法或者同步代碼塊的每一個線程,都看到由同一個鎖保護的以前全部的修改效果。換句話說,讀取一個非long或double類型的變量,能夠保證返回的值是某個線程保存在該變量中的,即便多個線程在沒有同步的狀況下併發地修改這個變量也是如此。

不要使用 Thread.stop方法。要阻止一個線程妨礙另外一個線程,建議作法是讓第一個線程輪訓一個boolean域,這個域一開始爲false,可是能夠經過第二個線程設置爲true,以表示第一個線程將終止本身。因爲boolean域的讀寫操做都是原子的,程序員在訪問這個域的時候再也不使用同步。

實際上,若是讀和寫操做沒有都被同步,同步就不會起做用。

若是變量修飾符是volatile,則讀取變量時不須要鎖,雖然volatile修飾符不執行互斥訪問,但它能夠保證任何一個線程在讀取該域的時候都將看到最近剛剛被寫入的值。

使用volatile的時候務必要當心。

//錯誤
private static volatile  int number = 0;
//須要使用synchronization
public static int getNumber() {
    return number++;
}
複製代碼

雖然number是原子的,可是增量操做符不是原子的,它首先讀取值,而後寫回一個新值。若是第二個線程在第一個線程讀取舊值和返回新值期間讀取這個域就會出錯。

避免過分同步

在一個被同步的區域內部,不要調用設計成要被覆蓋的方法,或者是由客戶端以函數對象的形式提供的方法。這樣的方法是外來的,這個類不知道方法會作什麼事情,也沒法控制它,從同步區域中調用它極可能會致使異常、死鎖或者數據損壞。

一般,你應該在同步區域內作儘量少的工做。若是你必需要執行某個很耗時的動做,應該設法把這個動做移到同步區域的外面。

executor 和 task 優先於線程

Java1.5增長了java.util.concurrent,這個包中包含了一個Executor Framework:

ExecutorService executorService = Executors.newSingleThreadExecutor();
//執行提交一個runnable方法
executorService.execute(runnable);
//告訴executor如何優雅地終止
executor.shutdonw();
複製代碼

你能夠利用executor service完成更多的事情。例如,能夠等待一個任務集合中的任何任務或全部任務完成(invokeAny或invokeAll),你能夠等待executor service優雅地完成終止(awaitTermination),能夠在任務完成時逐個地獲取這些任務的結果(ExecutorCompletionService)等。

併發工具優於wait和notify

自從java1.5髮型版本開始,java就提供了更高級的併發工具,他們能夠完成之前必須在wait和notify上手寫代碼來完成的各項工做。其分紅三類:

  • Executor Framework
  • 併發集合(Concurrent Collectionin)
  • 同步器(Synchronizer) 併發集合爲標準的集合接口(如List、Queue、Mpa)提供了高性能的併發實現。爲了提供高併發性,這些實如今內部本身管理同步,所以,併發集合中不可能排除併發活動,將它鎖定沒有什麼做用,只會是程序的速度變慢。

同步器(Synchronizer)是一些使線程可以等待另外一個線程的對象,容許他們協調動做。最經常使用的同步器是CountDownLatch和Semaphore。

倒計數鎖存器(CountDown Latch)是一次性的障礙,容許一個或者多個線程等待一個或者多個其餘線程來作某些事情。CountDownLatch是惟一構造器帶有一個int類型的參數,這個int參數是指容許全部在等待的線程被處理以前,必須在鎖存器上調用countDown方法的次數。

例如:一個方法帶有一個執行該動做的executor,一個併發級別(表示要併發執行該動做的次數),以及表示該動做的runnable。全部的工做線程自身都準備好,要在time線程啓動時鐘以前運行該動做(爲了實現準確的定時)。當最後一個工做線程準備好運行該動做時,timer線程就「發起頭炮」,同事容許工做線程執行該動做,一旦最後一個工做線程執行完該動做,timer線程就當即中止計時。直接在wait和notify上實現這個邏輯至少來講會很混亂,而在CountDownLatch之上實現則至關簡單:

public long getTime(Executor executor, int councurrency, final Runnable action) throws InterruptedException {
    final CountDownLatch ready = new CountDownLatch(councurrency);
    final CountDownLatch start = new CountDownLatch(1);
    final CountDownLatch done = new CountDownLatch(councurrency);
    for (int i = 0; i < councurrency; i++) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                ready.countDown();
                try {
                    start.await();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    done.countDown();
                }
            }
        });
    }
    ready.await();
    long startNano = System.nanoTime();
    start.countDown();
    done.await();
    return System.nanoTime() - startNano;
    }
複製代碼

用ready來告訴timer線程他們已經準備好了。而後工做線程會在start上等待。當最後一個工做線程調用ready.countDown時,timer線程記錄下起始時間,並調用start.countDown,容許全部的工做線程繼續進行。而後timer線程在done上等待,直到最後一個工做線程運行完該動做,並調用donw.countDown。一旦調用這個,timer線程就會甦醒過來,並記錄下結束時間。

wait方法的標準模式:

synchronized(obj) {
    while() {
        obj.wait(); //release lock, and reacquires on wakeup
    }
}
複製代碼

始終應該使用wait循環模式來調用wait方法;永遠不要在循環以外調用wait方法。循環會在等待以前和以後測試條件。

線程安全性的文檔化

線程安全性的幾種級別。(這份列表並無涵蓋全部的可能,而只是些常見的情形:

  • 不可變的(immutable):這個類的實例是不變的。因此不須要外部的同步,例如String、Long、BigInteger。
  • 無條件的線程安全(unconditionnally thread-safe):這個類的實例是可變的,可是這個類有着足夠的內部同步,因此它的實例能夠被併發使用,無需任何外部同步。 例如:Random和ConcurrentHashMa
  • 有條件的線程安全(conditionally thread-safe):除了有些方法爲進行安全的併發而使用須要外部同步
  • 非線程安全(not thread-safe):這個類的實例是可變的。爲了併發地使用它們,客戶必須利用本身選擇的外部同步包圍每一個方法調用(或者調用序列)。這樣的例子包括通用的集合實現,例如ArrayList和HashMap。
  • 線程對立的(thread-hostile):這個類不能安全地被多個線程併發使用,即便全部的方法調用都被外部同步包圍。線程對立的根源一般在於,沒有同步地修改靜態數據。Java平臺類庫中,線程對立的類或者方法很是少。System.runFinalizersOnExit方法是線程對立的,但已經被廢除了。
//私有鎖對象
private final Object lock = new Object();

public void foo() {
    synchronized(lock) {
        ...
    }
}
複製代碼

私有鎖對象模式只能用在無條件的線程安全類上。有條件的線程安全類不能使用這種模式,由於它們必須在文檔中說明:在執行某些方法調用序列時,它們的客戶端程序必須得到哪把鎖。

私有鎖對象模式特別適用於那些專門爲繼承而設計的類。若是這種類使用它的實例做爲鎖對象,之類可能很容易在無心中妨礙基類的操做,反之亦然,出於不一樣的目的而使用相同的鎖,子類和基類極可能會「互相絆住對方的腳」。

有條件的線程安全類必須在文檔中指明哪些方法調用序列須要外部同步,以及在執行這些序列的時候須要得到哪把鎖。若是你編寫的是無條件的線程安全類,就應該考慮使用私有鎖對象來代替同步的方法以防止客戶端程序和子類的不一樣步干擾。

慎用延遲初始化

若是處於性能的考慮須要對靜態域使用延遲初始化:

private static class FieldHolder {
    static final FieldType field = computeFieldValue();
}

static FieldHolder getField() {
      
複製代碼

若是處於性能的考慮須要對實例域使用延遲初始化:

private volatile FieldType field;
    
FieldTpye getField() {
    FieldType result = field;
    if(result == null) { //First check(no locking)
        synchronized (this) {
            result = field;
            if(result == null) //Second check(with locking)
                field = result = computeFieldValue();
        }
    }
    return result;
}
複製代碼

若是須要延遲初始化一個能夠接受重複初始化的實例域:

private volatile FieldType field;
    
private FieldType getField() {
    FieldType result = field;
    if(result == null) {
        field = result = computeFiedlValue();
    }
    return  result;
}
複製代碼

不要依賴於線程調度器

線程不該該一直處於忙-等狀態,即反覆地檢查一個共享對象,以等待某些事情的發生。

不要讓應用程序的正確性依賴於線程調度器,不然結果獲得的應用程序將既不健壯,也不具備可移植性。不要依賴Thread.yield或者線程優先級。線程優先級能夠用來提升一個已經可以正常工做的程序的服務質量,但永遠不該該用來「修正」一個本來能不能工做的程序。

序列化

謹慎地實現Serializable接口

實現Serializable接口而付出的巨大代價是,一旦一個類被髮布,就大大下降了「改變這個類的實現」的靈活性。 若是一個類實現了Serializable接口,它的字節流編碼(序列化形式)就變成了它的導出的API的一部分,一旦這個類被普遍使用,每每必須永遠支持這種序列化形式。

第二個代價是,它增長了出現bug和安全漏洞的可能性。你可能會忘記確保:反序列化過程必須也要保證全部「由真正的構造器創建起來的約束關係」,而且不容許攻擊者訪問正在構造過程當中的對象的內部信息。

第三個代價是,隨着類發行新的版本,相關的測試負擔也增長了。可序列化的類被修訂後,你必須既要確保「序列化-反序列化」過程成功,也要確保結果產生的對象真正是原始對象的複製品。

內部類不該該實現Serializable。

若是一個類爲了繼承而設計,要更加當心。對於這樣的類而言,在「容許子類實現Serializable接口」或者「禁止子類實現serialzable」二者間的一個折衷方案是:提供一個可訪問的無參構造器,這種方案容許(但不要求)子類實現Serializable接口。

相關文章
相關標籤/搜索