1. 程式人生 > >Effective Java學習筆記一

Effective Java學習筆記一

一.考慮使用靜態工廠方法代替構造器
優點:①靜態方法有合適的名字。
 ②不必在每次呼叫他們的時候都建立一個新物件
 ③靜態方法可以返回型別的任何子型別物件。
 ④建立引數化型別例項的時候,它們使程式碼變得更加簡潔
    服務提供者框架中有三個重要的元件:服務介面(提供者實現的),提供者註冊API(系統用來註冊實現的), 服務訪問API(客戶端用來獲取服務的)
缺點:①如果不含公有的或者受保護的構造器,就不能被子類化。
     ②它們與其他的靜態方法實際上沒有任何區別。
靜態方法常用的名稱:valueOf, of, getInstance, newInstance, getType, newType

二.遇到多個構造器引數時要考慮用構建器

重構構造器模式可行,但是當有許多引數的時候,客戶端程式碼會很難編寫,並且仍然較難以閱讀。
第一種方式是過載多個建構函式
第二種方式是JavaBeans模式(缺點是在構造過程中JavaBean可能處於不一致的狀態,javaBean模式阻止了把類做成不可變的可能)
第三種方式是Builder模式,使用構造器或者靜態工廠生成物件,然後設定每個相關的可選引數。
    平時使用Builder模式的時候,我們可以事先建立一個Builder泛型介面,然後每次都繼承Builder介面
傳統的抽象工廠是使用Class物件的newInstance方法充當builder的一部分, 然而newInstance總是試圖呼叫無參的建構函式,可能產生異常。
builder的缺點:為了建立物件,必須先建立它的構造器,有一定的效能損耗。

三.用私有構造器或者列舉型別強化Singleton屬性

Singleton就是我們常說的單例,代表那些本質上是唯一的元件。
①可以將構造器設為私有的,避免其違反Singleton的規則。如果使用反射機制,反射會忽略許可權,
可能也會使用私有的建構函式,此時我們可以加上一些我們的判斷。
②公有的成員是個靜態工廠方法,公有的靜態域是final的,所以該域總是包含相同的物件引用,
使用這種方法不會帶來效能上的損失,因為虛擬機器已經實現了靜態
工廠方法的內聯呼叫
為了保證Singleton,必須宣告所有例項域都是瞬時的(transient),並且提供readResolve方法。
③列舉方式

四.通過私有構造器強化不可例項化的能力
有時用java實現演算法題的時候編寫只包含靜態方法和靜態域的類,這是一種很不好的習慣。
但是有時工具類也需要這樣,這些類是希望不被例項化的。
企圖通過將類做成抽象類來強制該類不能被例項化,這是行不通的,這些類可以被子類化。
解決方法:讓類中所有的構造器都程式設計私有的(謹記,系統提供的預設構造器是顯式的)
輔作用:該類不能被子類化。


五.避免建立不必要的物件

如果是常量的話,可以採用字面常量,這樣使用常量池,不會每次都建立新的物件。
如果是物件的話,通常可以使用靜態工廠方法而不是構造器。
介面卡:它把功能委託給一個後備物件,從而為後備物件提供一個可以替代的介面
要優先使用基本型別而不是裝箱基本型別,要當心無意識的自動裝箱會導致效能的降低。
物件池:一般用來裝載重量級的物件,真正正確使用物件池的典型物件示例就是資料庫連線池。
這裡不能和保護性拷貝搞混,因為在那裡重用的話可能會導致潛在的錯誤和安全漏洞。


六.消除過期的物件引用
如果一個物件引用被無意識地保留起來,那麼垃圾回收機制不僅不會處理這個物件,而且不會垂柳被這個物件引用的所有其他物件
解決方式:一旦物件引用已經過期,只需清空這些引用即可。
eg: public Object pop() {
if(size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null;    // this is necessary.
return result;
}
消除過期引用的最好的方法是:在最緊湊的作用於範圍內定義每一個變數,這種情況會自然而然的發生。
如果類是自己管理記憶體的,程式設計師就應該警惕記憶體洩露問題。
快取也是造成記憶體洩露的一個原因,常用的解決方式是定期的清理快取。
記憶體洩露的第三個常見來源是監聽器和其他回撥。你實現了一個API,客戶端在這個API中註冊回撥,卻沒有顯示地取消註冊,除非你採取某些措施
確保回撥立即被當作垃圾回收的最佳方法是隻儲存它們的弱引用。

七.避免使用終結方法
終結方法是不可預測的,也是很危險的,一般情況下不必要的。
在java一般使用try-catch的方式來完成c++解構函式的功能
終結方法被呼叫不是即使,並且終結方法的佇列優先順序比較低,並且終結方法依賴於JVM平臺。
圖形物件的終結速度達不到它們進入佇列的速度可能造成超記憶體的情況
java規範不僅不保證終結方法會被及時地執行,而且根本就不保證他們會被執行。
tip:不要依賴終結方法來更新重要的持久狀態。
System.gc System.runFinalization這兩個方法增加了終結方法被執行的機會,但是也並不保證終結方法一定會被執行。
如果未被捕獲的異常在終結過程中被丟擲來,那麼這種異常可能被忽略,並且該物件的終結過程也會終止。
終結方法有嚴重的效能損失。
①如果想將封裝的資源終止,只需要提供一個顯示的終止方法。
終結方法通常和try-finally結構結合起來使用,確保及時終止。
如果使用終結方法記住要呼叫父類的終結方法。
終結方法鏈是不會被自動執行的,如果類有終結方法,並且子類覆蓋了終結方法,子類的終結方法就必須手工呼叫超類的終結方法


八.覆蓋equals方法遵守通用約定
覆蓋equals的時機:
如果類具有自己特有的“邏輯相等”概念,並且超類沒有覆蓋equals以期望的行為。
對於那些每個值至多隻存在一個物件的情況,無需覆蓋equals方法,因為邏輯相等就等於物件相同。
equals的等價關係:
①自反性 ②對稱性 ③傳遞性 ④一致性 ⑤x非空,x.equals(null)必須返回false
關於Point, ColorPoint的問題可以顯示出一個問題:我們無法在擴充套件可例項化的類的同時,既增加新的值元件,同時又保留equals約定。
平時實現equals時都會首先用instanceof進行判斷。
實現equals方法時需要注意里氏替換原則,一個型別的任何重要屬性也將適用於它的子型別。
無論類是否可變,都不要讓equals方法依賴於不可靠的資源
equals方法的訣竅:
①使用==操作符檢查引數是否為這個物件的引用。
②使用instanceof操作符檢查“引數是否為正確的型別”
③把引數轉換成正確的型別。
④對於該類中的每個“關鍵”域,檢查引數中的域是否與該物件中對應的域相匹配。
注意:基本型別中除了float和double使都用==操作符,物件使用equals,float和double使用Compare方法。
對於部分允許為空的域可以採用下面的程式碼來進行優化:
field == null ? o.field == null : field.equals(o.field))
為了獲得高效能,應該最先比較最有可能不一致的域,或者是低開銷的域。
覆蓋eqeuals時總要覆蓋hashCode方法


九.覆蓋equals時總要覆蓋hashCode方法
如果不這樣做的話,很可能會違反Object,hashCode的通用約定,使該類無法結合所有基於雜湊點的集合一起正常工作。
如果兩個物件equals方法比較是相等的,那麼呼叫者兩個物件中任意一個物件的hashCode方法都必須產生同樣的結果。
如果兩個物件根據equals(Object)方法比較是不相等的,那麼呼叫這兩個物件中任意一個物件的hashCode方法,則不一定要產生不同的整數結果
Hash中,可以將與每個項關聯點的雜湊碼快取起來,如果雜湊碼不匹配,也不必檢驗物件的等同性。
hashCode方法的實現的準則:
①把某個非零的常數值,儲存到一個名為result的int型別變數中
②對於物件中每個關鍵域f,完成以下步驟
(1)如果該域是boolean型別,則計算(f ? 1 : 0)
(2)如果該域是byte,char,short或者是int型別,則計算(int)f
 (3) 如果該域是long型別,則計算(int)(f ^ (f >> 32))
 (4) 如果該域是float,則計算Float.floatToIntBits(f), double一樣
 (5) 如果是物件則遞迴呼叫equals方法
 (6) 如果該域是一個數組,則要把每一個元素當做單獨的域來處理
按照下面的公式,計算得到的雜湊碼c合併到result中:
result = 31 + result + c
返回result
在雜湊碼的計算過程中,可以把冗餘域排除在外,必須排除equals比較計算中沒有用到的任何域。
平時我們可以使用31來代替以為操作(31為低位全1,可以防止資訊會丟失)
如果一個類是不可變的, 並且計算雜湊碼的開銷比較大,就應該考慮把雜湊碼快取在物件內部

 十.始終要覆蓋toString方法
    Object提供了toString的實現  包含類的名稱 + @ + hashCode(16進位制的表示)
無論是否指定格式,都為toString返回值中包含的所有資訊,提供一種程式設計式的訪問途徑

 十一.謹慎地覆蓋clone
Cloneable介面的目的是表明這樣的物件允許克隆,其主要的缺陷在於它缺少一個clone方法,
Cloneable方法中沒有任何方法,它是為了改變超類中受保護的方法的行為。
拷貝的精確含義: x.clone() != x   x.clone().getClass() == x.getClass()  x.clone().equals(x)
super.clone只是淺層次的拷貝
注意:如果elements域是final的,上述方案就不能正常工作,因此在super.clone之後elements的值是不可改變的,
所以,如果想讓類是可克隆的,可能有必要從某些域中去掉final修飾符。
克隆複雜物件的最後一種方法是:先呼叫super.clone, 然後把物件中的所有域都設定成它們的空白狀態,然後呼叫搞成的克隆函式
總結:所有實現了Cloneable介面的類都應該用一個公有的方法覆蓋clone方法,此公有方法首先呼叫super.clone,然後修正任何需要修正的域
如果你擴充套件了實現了Cloneable介面的類,那麼你除了實現一個行為良好的clone方法外,沒有別的選擇
拷貝的另外一種實現是:一個實現物件拷貝的好方法是提供一個拷貝構造器或拷貝工廠。
eg: public Yum(Yum yum); public static Yum newInstance(Yum yum);
上面的兩種方法都要比Cloneable/clone方法具有更多的優勢。

 十二.考慮實現Comparable介面
一旦類實現了Comparable介面,它就可以跟許多泛型演算法以及依賴於該介面的集合實現進行協作
由compareTo方法施加的等同性測試,也一定遵守自反性,對稱性和傳遞性
TreeSet和TreeMap中使用的比較就是compareTo方法
如果一個域並沒有實現Comparable介面,或者你需要使用一個非標準的排序關係,就可以使用一個顯示的Comparator來代替
compareTo方法約定並沒有指定返回值的大小,可以利用這一點來進行簡化程式碼(這裡需要注意別超過int的最大範圍)
eg: public int compareTo(PhoneNumber pn) {
int areaCodeDiff = areaCode - pn.areaCode;
if(areaCodeDiff != 0)
return areaCodeDiff;
//...
return 0;
}


 十三.使類和成員的可訪問性最小化
又名資訊隱藏或封裝 
資訊隱藏之所以重要的原因:他可以有效的解除組成系統的各模組之間的耦合關係
規則1:儘可能使每個類或者成員不被外界訪問,只有兩種可能的訪問級別:包級私有的和公有的
可訪問性的遞增順序:
①私有的(private): 私有的——只有宣告該成員的頂層類才可以訪問這個成員
②預設: 該成員的包內部的任何類都可以訪問這個成員
③受保護的(protected): 宣告該成員的類的子類可以訪問這個成員
④公有的(public):  在任何地方都可以訪問該成員
當你仔細設計了類的公有API之後,可能覺得應該把所有其他的成員都變成私有的
訪問性的規定:如果方法覆蓋了超類中的一個方法,子類中的訪問級別就不允許低於超類中的訪問級別,
這樣可以確保任何可使用超類的例項的地方也都可以使用子類的例項
為了便於測試,一般將級別宣告為包級私有的,可以讓測試作為被測試的包的一部分來執行,從而能夠訪問它的包級私有元素
例項域不能是公有的,一旦將其公有話,就放棄了對儲存在這個域中的值進行限制的能力
長度非零的陣列總是可變的,所以,類具有公有的靜態final陣列域,這幾乎是錯的。


 十四.在公有類中使用訪問方法而非公有域
這也就是平常使用的setter getter模式
如果類可以在它所在的包的外部進行訪問,就提供訪問方法




 十五.使可變性最小化
    為了使類稱為不可變類,要遵循下面五條規則:
1.不要提供任何修改物件狀態的方法
2.保證類不會被擴充套件
3.使所有的域都是final的
4.使所有的域都稱為私有的
5.確保對於任何可變元件的互斥訪問
函式式的做法可以保證物件的狀態不會被改變
過程的或者命令式的做法可能會導致它的狀態發生改變
如果一個物件具有不可變性,可以將其做成一個不可變物件,這會帶來大量的好處
不可變類真正唯一的缺點是,對於每個不同的值都需要一個單獨的物件。

  十六.複合優先於繼承
子類脆弱的原因(overriding中出現的問題):
①與方法呼叫不同,繼承打破了封裝性,子類依賴於其超類中特定功能的實現細節,因為在子類中可能呼叫非public的方法
②超類在後面的髮型版本中增加新的方法,而子類無法改變具體實現了,只能使用舊版本的方法實現。
上面的這些問題可以通過改用複合的方式得以解決,新類中的每個例項方法都可以呼叫所包含的元件中的對應方法,這被稱之為轉發。
用繼承的時機:當兩者之間真正滿足“is - a”關係的時候才會使用繼承

  十七.要麼為繼承而設計,並提供文件說明,要麼就禁止繼承
    好的API文件應該秒速一個給定的方法做了什麼工作而不是描述其是如何做到的
為了允許繼承,類還必須遵守其他一些約定。構造器決不能呼叫可被覆蓋的方法,無論是直接呼叫還是間接呼叫
為了繼承而設計類的時候,Cloneable和Serializable接口出現了特殊的困難,無論實現著其中的哪個介面通常都不是好主意
因為clone和readObject方法在行為上非常類似於構造器,所以類似的限制規則也是使用的, 無論是clone還是readObject,都不能呼叫可覆蓋的方法
對於普通的具體類,它們既不是final的,也不是為了子類化而設計和編寫文件的,這種狀態很危險
這個問題的最佳解決方案是,對於那些並非為了安全地進行子類化而設計和編寫文件的類,要禁止子類化

  十八.介面優於抽象類
介面和抽象類這兩種機制都用用來允許多個實現的型別
介面的優點:
①抽象類會間接傷害到類層次
②介面是定義mixin的理想選擇(mixin是允許任選的功能可被混合都型別的主要功能=中)
③介面允許我們構造非層次結構的型別框架
骨架實現上有個小小的不同,就是簡單實現,AbstractMap,SimpleEntry就是個例子
eg:SimpleEntry, AbstarctMap  簡單實現就像個骨架實現,因為它實現了介面,並且是為了繼承而設計的,他是最簡單的可能的有效實現
    你可以原封不動的使用,也可以看情況將它子類化
 抽象類的優勢:抽象類的演變比介面的演變要容易得多


  十九.介面只用於定義型別
一旦類實現了某個介面,就表明客戶端可以對這個類的例項實施某些動作
如果要匯出常量的話,常量介面模式是對介面的不良使用,它的所有子類的名稱空間會被介面中的常量所“汙染”
匯出常量的合理方案:
①如果這些常量與某個現有的類或者介面緊密相關,就應該把這些常量新增到這個類或者介面中
②如果這些常量最好被看作列舉型別的成員,就應該用列舉型別來匯出這些常量
③使用不可例項化的工具類來匯出這些常量
介面應該製備用來定義型別,它們不應該被用來匯出常量

  二十.類層次優於標籤類
    面向物件就提供了其他更好的方法來定義能表示多種風格物件的單個數據型別:子類化
為了將標籤轉變成類層次,首先要為標籤類中的每個方法都定義一個包含抽象方法的抽象類,這每個方法的行為都依賴標籤值
類層次的另一個好處在於,它們可以用來反映型別之間本質上的層次關係,有助於增強靈活性,並進行更好的編譯時型別檢查

  二十一.用函式物件表示策略
    c語言標準庫中的qsort函式要求用一個指向comparator函式的指標作為引數,比較函式有兩個引數,返回值根據兩者的大小情況而定
java中的Comparator也擁有同樣的功能,這也是策略模式的一個例子
java沒有提供函式指標,但是可以用物件引用實現同樣的功能
我們在設計具體的策略類時,還需要定義一個策略介面

  二十二.優先考慮靜態成員類
    巢狀類存在的目的應該只是為了它的外圍類提供服務。巢狀類有四種類型:
①靜態成員類 ②非靜態成員類 ③匿名類 ④區域性類
靜態成員類可以訪問外部類的所有成員,靜態成員類是外圍類的一個靜態成員,也遵循同樣的可訪問性規則
非靜態成員類雖然和靜態成員類最明顯的區別是static的有無,但是每個非靜態例項都隱含著與外圍類的一個外圍相關聯,
並且非靜態成員類的類中可以呼叫外圍物件的方法或者域
如果生命成員類不要求訪問外圍例項,就要始終把static修飾符放在它的宣告中。
私有靜態成員類的一種常見用法是用來代表外圍類類所代表的物件的元件
匿名類不同於其他的任何語法單元,匿名類沒有名字,在使用的同時被宣告和例項化
匿名類不能執行instanceof測試,或者做任何需要命名類的其他事情,可以用來建立函式物件或者多執行緒類。
區域性類是四種巢狀類中用得很少的類,在任何可以宣告區域性變數的地方都可以宣告區域性類,並且區域性類也遵守同樣的作用域規則

  二十三.請不要在新程式碼中使用原生態型別
    有了泛型就可以限制容器中裝載的物件的型別了
如果使用原生態型別,就失掉了泛型在安全性和表述性方面的所有優勢,現在的原生態型別就是為了相容沒有泛型之前的程式碼。
List和List<Object>, 前者使用原生態型別,會失掉型別安全性,但是如果使用像List<Object>這樣的引數化型別,則不會
java提供了一種安全的替代方法,稱作無限制的萬用字元型別,如果要使用泛型,但不確定或者不關心實際的型別引數,就可以使用?代替
Set<?>和原生態型別Set之間的區別:萬用字元型別是安全的,原生態型別則不是安全,
由於可以將任何元素放進使用原生態型別的集合中,因此很容易破壞該集合的型別約束條件,所以Set<?>只允許放null型別
使用原生態型別的情況:
①在類文字中必須使用原生態型別,因為List<?>.class是不合法的
②因為泛型資訊可以在執行時被檫除,因此在引數化型別而非無限制萬用字元型別上使用instanceof是非法的,故得用原生態型別
Set<Object>是個引數化型別,表示可以包含任何物件型別的一個集合
Set<?>是一個萬用字元型別,表示只能包含某種未知物件型別的一個集合.
Set則是個原聲態型別
 
  二十四.消除非受檢警告
當遇到警告時,要儘可能地消除每一個非受檢警告,如果消除了所有警告,就可以確保程式碼是型別安全的
如果無法消除警告,同時可以保證引起警告的程式碼是類=型別安全的,可以用一個@SuppressWarnings("unchecked")註解來禁止這條警告
SuppressWarnings註解可以用在任何粒度的級別中,從單獨的區域性變數宣告到整個類都可以。
將SuppressWarnings註解放在return語句中是非法的,因為它不是一個宣告。
每當使用SuppressWarnings都要新增一條註釋,說明為什麼這麼做是安全的。

  二十五.列表優先於陣列
    列表和陣列的兩個重要的不同點:
①陣列是協變的, Sub是Super的子型別,Sub[]是Super[]的子型別,但是泛型是不可變的
②陣列是具體化的,陣列會在執行的時候才知道並檢查它們的元素型別約束,但是泛型是執行時檫除的,檫除使得泛型可以在沒有使用泛型的程式碼中隨意互用
上面的區別造成兩者不能很好的混合使用
不可具體化的型別是指其執行時表示法包含的資訊比它的編譯時表示法包含的資訊更少的型別。
唯一可以具體化的就是無限制的萬用字元型別, List<?>, Map<?, ?>
當你想使用泛型陣列時,可以使用泛型集合來解決