創建和銷毀對象
靜態工廠模式
- 構造器里未傳參的成員不會被初始化。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里的實現繼承下來。
使用裝飾者模式:ForwardingSet<E> implements Set<E>,該類有成員private final Set<E> s s,構造器里就是傳入一個Set<E> ,該類不具體實現Set的任何方法,例如:
public boolean add(E e) {
return s.add(e);
}
InstrumentedSet<E> extends ForwardingSet<E>,構造器super父類即可,在這個類里添加一些功能,例如:
@Override
public boolean add(E e){
count++;
return super.add(e);
}
這種模式下,InstrumentedSet 只是一個包裝類,只是對其成員Set<Set>進行修飾,為它增加計數特性。包裝類并不實現具體功能,構造器里傳入的就是實現具體功能的Set,可以是HaseSet或者自己實現的Set。
另可參考閱讀:
Android源碼學習之裝飾模式應用
繼承后構造方法的調用
- 如果子類沒有定義構造方法,則調用父類的無參數的構造方法。
- 如果子類定義了構造方法,不論是無參數還是帶參數,在創建子類的對象的時候,首先執行父類無參數的構造方法,然后執行自己的構造方法。
- 如果子類調用父類帶參數的構造方法,可以通過super(參數)調用所需要的父類的構造方法,切該語句做為子類構造方法中的第一條語句。
- 如果某個構造方法調用類中的其他的構造方法,則可以用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,因此私有的靜態成員類是最佳的選擇。
- 如果一個嵌套類需要在單個方法之外可見,或者它太長了不適合放在方法內部,就使用成員類。
- 如果成員類的每個實例都需要一個指向外圍實例的應用,就使用非靜態成員類。否則就使用靜態成員類。
- 如果嵌套類屬于一個方法的內部,且你只需要在一個地方創建實例,并且已經有了一個預置的類型可以說明這個類的特征,就使用匿名類。否則就使用局部類。
泛型
列表優先于數組
二者的不同點:
數組是協變的(covariant)
如果B是A的子類,那么B[]就是A[]的子類型。
//編譯時不報錯,運行時報錯ArrayStoreException
Object[] test = new Long[1];
test[0] = "test";
而兩個不同的類型A、B,List<A>既不是List<B>的子類也不是超類。
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接口)
- 標記接口勝過標記注解的兩點:
- 接口定義的類型是由被標記類的實例實現的,注解則沒有定義這樣的類型。這個類型允許你在編譯時捕捉到錯誤,而不像注解需要在運行時才能捕捉到
- 接口可以被更加精確地鎖定。假設一個標記只適用于特殊接口的實現,如果定義成標記接口就可以用它將唯一的接口擴展成它適用的接口。
- 注解勝過接口的兩點:
- 注解可以不斷演變。而接口通常不可能在實現后再給它添加方法。
- 注解是注解機制的一部分。注解可以作為支持注解作為編程元素之一的框架中具有一致性。
- 接口和注解使用場景:
- 如果標記是應用到任何程序元素而不是類或接口,就必須使用注解,因為只有類和接口可以用來實現或擴展接口。
- 如果標記只給類和接口,若要編寫多個只接受有這種標記的方法則優先使用接口,這樣可以在編譯時進行類型檢查。
- 如果要永遠限制這個標記只用于特殊接口的元素,最好將標記定義成該接口的一個子接口。
- 如果2,3都是否定的,則應該使用注解。
方法
檢查參數的有效性
- assert 對于有些參數,方法本身沒有用到,卻被保存起來供以后使用,可以使用斷言檢驗這類參數的有效性。如果斷言失敗,則會拋AssertionError。
必要時進行保護性拷貝
如果類的成員是可變的,為了保護內部信息變化,對于構造器的每個可變can'shu參數進行保護性拷貝是必要的,使用被封對象作為實例的組件,而不使用原始的對象。但注意,保護性拷貝是在檢查參數的有效性之前進行的,并且有效性檢查是針對拷貝之后的對象而不是原始對象。
慎用clone。如果對于非final的成員,不能保證clone方法一定返回同樣的類的對象,它有可能返回專門出于惡意目的而設計的不可信子類的實例,例如這樣的子類可以在每個實例被創建時把指向該實例的引用記錄到一個私有的靜態列表中,并且允許攻擊者訪問這個列表,這將使得攻擊者可以自由地控制所有的實例。為了阻止這種攻擊,對于參數類型可以被不可信任方子類話的參數,請不要使用clone方法進行保護性拷貝。
-
另外需要修改訪問方法,返回可變內部域的保護性拷貝:
public Data end() { return new Data(end.getTime()); }
只要可能,都應該使用不可變的對象作為對象內部的組件,這樣就不必再為保護型拷貝操心。
慎用重載
類型還是父類,雖然調用父類方法指向子類引用。
安全而保守的策略是:永遠不要導出兩個具有相同參數數目的重載方法。如果方法使用可變參數,保守的策略是根本不要重載它。
慎用可變參數
-
如果客戶端調用這個方法時并沒有傳遞參數進去,它就會在運行時而不是編譯時失敗。
//帶兩個參數,避免沒有傳參導致的問題 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的情況:
- 過濾——需要遍歷集合并刪除選定元素
- 轉換——需要遍歷集合并取代它的部分或全部元素值
- 平行迭代——需要并行地遍歷多個集合,就需要顯式地控制迭代器或者索引變量以便所有迭代器或索引變量都可以得到同步前移
了解和使用類庫
-
偽隨機數生成器
//錯誤 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)。有幾種途徑可以實現這種效果:
- 在執行操作前檢查參數的有效性,這可以使在對象狀態被修改前先拋出適當的異常。
- 調整計算處理過程的順序,使得任何可能會失敗的計算部分都在對象狀態被修之前發生。
- 編寫一段恢復代碼,由它來攔截操作過程發生的失敗,以及使對象回滾到操作開始之前的狀態,這種辦法主要用于永久性的數據結構。
- 在對象的一份臨時拷貝上執行操作,操作完成之后再用臨時拷貝中的結果代替對象的內容。
不要忽略異常
忽略一個異常非常容易,只需將方法調用通過try語句包圍起來,并包含一個空的catch塊??盏腸atch塊會使異常達不到應有的目的,至少,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接口。