《Effective Java》筆記(上)
對象的創建與銷毀
- Item 1: 使用static工廠方法,而不是構造函數創建對象:僅僅是創建對象的方法,并非Factory Pattern
- 優點
- 命名、接口理解更高效,通過工廠方法的函數名,而不是參數列表來表達其語義
- Instance control,并非每次調用都會創建新對象,可以使用預先創建好的對象,或者做對象緩存;便于實現單例;或不可實例化的類;對于immutable的對象來說,使得用
==
判等符合語義,且更高效; - 工廠方法能夠返回任何返回類型的子類對象,甚至是私有實現;使得開發模塊之間通過接口耦合,降低耦合度;而接口的實現也將更加靈活;接口不能有static方法,通常做法是為其再創建一個工廠方法類,如Collection與Collections;
- Read More: Service Provider Framework
- 缺點
- 僅有static工廠方法,沒有public/protected構造函數的類將無法被繼承;見仁見智,這一方面也迫使開發者傾向于組合而非繼承;
- Javadoc中不能和其他static方法區分開,沒有構造函數的集中顯示優點;但可以通過公約的命名規則來改善;
-
小結
static工廠方法和public構造函數均有其優缺點,在編碼過程中,可以先考慮一下工廠方法是否合適,再進行選擇。 - Item 2: 使用當構造函數的參數較多,尤其是其中還有部分是可選參數時,使用Builder模式
- 以往的方法
- Telescoping constructor:針對可選參數,從0個到最多個,依次編寫一個構造函數,它們按照參數數量由少到多逐層調用,最終調用到完整參數的構造函數;代碼冗余,有時還得傳遞無意義參數,而且容易導致使用過程中出隱蔽的bug;
- JavaBeans Pattern:靈活,但是缺乏安全性,有狀態不一致問題,線程安全問題;
- Builder Pattern
- 代碼靈活簡潔;具備安全性;
- immutable
- 參數檢查:最好放在要build的對象的構造函數中,而非builder的構建過程中
- 支持多個field以varargs的方式設置(每個函數只能有一個varargs)
- 一個builder可以build多個對象
- Builder結合泛型,實現Abstract Factory Pattern
- 傳統的抽象工廠模式,是用Class類實現的,然而其有缺點:newInstance調用總是去調用無參數構造函數,不能保證存在;newInstance方法會拋出所有無參數構造函數中的異常,而且不會被編譯期的異常檢查機制覆蓋;可能會導致運行時異常,而非編譯期錯誤;
-
小結
Builder模式在簡單地類(參數較少,例如4個以下)中,優勢并不明顯,但是需要予以考慮,尤其是當參數可能會變多時,有可選參數時更是如此。 - Item 3: 單例模式!
不管以哪種形式實現單例模式,它們的核心原理都是將構造函數私有化,并且通過靜態方法獲取一個唯一的實例,在這個獲取的過程中你必須保證線程安全、反序列化導致重新生成實例對象等問題,該模式簡單,但使用率較高。 - double-check-locking
```java
private static volatile RestAdapter sRestAdapter = null;
public static RestAdapter provideRestAdapter() {
if (sRestAdapter == null) {
synchronized (RestProvider.class) {
if (sRestAdapter == null) {
sRestAdapter = new RestAdapter();
}
}
}
return sRestAdapter;
}
```
DCL可能會失效,因為指令重排可能導致同步解除后,對象初始化不完全就被其他線程獲??;使用volatile關鍵字修飾對象,或者使用static SingletonHolder來避免該問題(后者JLS推薦);
- class的static代碼:一個類只有在被使用時才會初始化,而類初始化過程是非并行的,這些都由JLS能保證
- 用enum實現單例
- 還存在反射安全性問題:利用反射,可以訪問私有方法,可通過加一個控制變量,該變量在getInstance函數中設置,如果不是從getInstance調用構造函數,則拋出異常;
- Item 4: 將構造函數私有化,使得不能從類外創建實例,同時也能禁止類被繼承
util類可能不希望被實例化,有其需求 - Item 5: 避免創建不必要的對象
- 提高性能:創建對象需要時間、空間,“重量級”對象尤甚;immutable的對象也應該避免重復創建,例如String;
- 避免auto-boxing
- 但是因此而故意不創建必要的對象是錯誤的,使用object pool通常也是沒必要的
- lazy initialize也不是特別必要,除非使用場景很少且很重量級
- Map#keySet方法,每次調用返回的是同一個Set對象,如果修改了返回的set,其他使用的代碼可能會產生bug
- 需要defensive copying的時候,如果沒有創建一個新對象,將導致很隱藏的Bug
- Item 6: 不再使用的對象一定要解除引用,避免memory leak
- 例如,用數組實現一個棧,pop的時候,如果僅僅是移動下標,沒有把pop出棧的數組位置引用解除,將發生內存泄漏
- 程序發生錯誤之后,應該盡快把錯誤拋出,而不是以錯誤的狀態繼續運行,否則可能導致更大的問題
- 通過把變量(引用)置為null不是最好的實現方式,只有在極端情況下才需要這樣;好的辦法是通過作用域來使得變量的引用過期,所以盡量縮小變量的作用域是很好的實踐;注意,在Dalvik虛擬機中,存在一個細微的bug,可能會導致內存泄漏,詳見
- 當一個類管理了一塊內存,用于保存其他對象(數據)時,例如用數組實現的棧,底層通過一個數組來管理數據,但是數組的大小不等于有效數據的大小,GC器卻并不知道這件事,所以這時候,需要對其管理的數據對象進行null解引用
- 當一個類管理了一塊內存,用于保存其他對象(數據)時,程序員應該保持高度警惕,避免出現內存泄漏,一旦數據無效之后,需要立即解除引用
- 實現緩存的時候也很容易導致內存泄漏,放進緩存的對象一定要有換出機制,或者通過弱引用來進行引用
- listner和callback也有可能導致內存泄漏,最好使用弱引用來進行引用,使得其可以被GC
- Item 7: 不要使用finalize方法
- finalize方法不同于C++的析構函數,不是用來釋放資源的好地方
- finalize方法執行并不及時,其執行線程優先級很低,而當對象unreachable之后,需要執行finalize方法之后才能釋放,所以會導致對象生存周期變長,甚至根本不會釋放
- finalize方法的執行并不保證執行成功/完成
- 使用finalize時,性能會嚴重下降
- finalize存在的意義
- 充當“safety net”的角色,避免對象的使用者忘記調用顯式termination方法,盡管finalize方法的執行時間沒有保證,但是晚釋放資源好過不釋放資源;此處輸出log警告有利于排查bug
- 用于釋放native peer,但是當native peer持有必須要釋放的資源時,應該定義顯式termination方法
- 子類finalize方法并不會自動調用父類finalize方法(和構造函數不同),為了避免子類不手動調用父類的finalize方法導致父類的資源未被釋放,當需要使用finalize時,使用finalizer guardian比較好:
- 定義一個私有的匿名Object子類對象,重寫其finalize方法,在其中進行父類要做的工作
- 因為當父類對象被回收時,finalizer guardian也會被回收,它的finalize方法就一定會被觸發
Object的方法
盡管Object不是抽象類,但是其定義的非final方法設計的時候都是希望被重寫的,finalize除外。
- Item 8: 當重寫equals方法時,遵循其語義
- 能不重寫equals時就不要重寫
- 當對象表達的不是值,而是可變的狀態時
- 對象不需要使用判等時
- 父類已重寫,且滿足子類語義
- 當需要判等,且繼承實現無法滿足語義時,需要重寫(通常是“value class”,或immutable對象)
- 當用作map的key時
- 重寫equals時需要遵循的語義
- Reflexive(自反性): x.equals(x)必須返回true(x不為null)
- Symmetric(對稱性): x.equals(y) == y.equals(x)
- Transitive(傳遞性): x.equals(y) && y.equals(z) ==> x.equals(z)
- Consistent(一致性): 當對象未發生改變時,多次調用應該返回同一結果
- x.equals(null)必須返回false
- 實現建議
- 先用==檢查是否引用同一對象,提高性能
- 用instanceof再檢查是否同一類型
- 再強制轉換為正確的類型
- 再對各個域進行equals檢查,遵循同樣的規則
- 確認其語義正確,編寫測例
- 重寫equals時,同時也重寫hashCode
- !重寫equals方法,傳入的參數是Object
- Item 9: 重寫equals時也重寫hashCode函數
- 避免在基于hash的集合中使用時出錯
- 語義
- 一致性
- 當兩個對象equals返回true時,hashCode方法的返回值也要相同
- hashCode的計算方式
- 要求:equals的兩個對象hashCode一樣,但是不equals的對象hashCode不一樣
- 取一個素數,例如17,result = 17
- 對每一個關心的field(在equals中參與判斷的field),記為f,將其轉換為一個int,記為c
- boolean: f ? 1 : 0
- byte/char/short/int: (int) f
- long: (int) (f ^ (f >> 32))
- float: Float.floatToIntBits(f)
- double: Double.doubleToLongBits(f),再按照long處理
- Object: f == null ? 0 : f.hashCode()
- array: 先計算每個元素的hashCode,再按照int處理
- 對每個field計算的c,result = 31 * result + c
- 返回result
- 編寫測例
- 計算hashCode時,不重要的field(未參與equals判斷)不要參與計算
- Item 10: 重寫toString()方法
- 增加可讀性,簡潔、可讀、具有信息量
- Item 11: 慎重重寫clone方法
- Cloneable接口是一個mixin interface,用于表明一個對象可以被clone
- Contract
- x.clone() != x
- x.clone().getClass() == x.getClass():要求太弱,當一個非final類重寫clone方法的時候,創建的對象一定要通過super.clone()來獲得,所有父類都遵循同樣的原則,如此最終通過Object.clone()創建對象,能保證創建的是正確的類實例。而這一點很難保證。
- x.clone().equals(x)
- 不調用構造函數:要求太強,一般都會在clone函數里面調用
- 對于成員變量都是primitive type的類,直接調用super.clone(),然后cast為自己的類型即可(重寫時允許返回被重寫類返回類型的子類,便于使用方,不必每次cast)
- 成員變量包含對象(包括primitive type數組),可以通過遞歸調用成員的clone方法并賦值來實現
- 然而上述方式違背了final的使用協議,final成員不允許再次賦值,然而clone方法里面必須要對其賦值,則無法使用final保證不可變性了
- 遞歸調用成員的clone方法也會存在性能問題,對HashTable遞歸調用深拷貝也可能導致StackOverFlow(可以通過遍歷添加來避免)
- 優雅的方式是通過super.clone()創建對象,然后為成員變量設置相同的值,而不是簡單地遞歸調用成員的clone方法
- 和構造函數一樣,在clone的過程中,不能調用non final的方法,如果調用虛函數,那么該函數會優先執行,而此時被clone的對象狀態還未完成clone/construct,會導致corruption。因此上一條中提及的“設置相同的值”所調用的方法,要是final或者private。
- 重載類的clone方法可以省略異常表的定義,如果重寫時把可見性改為public,則應該省略,便于使用;如果設計為應該被繼承,則應該重寫得和Object的一樣,且不應該實現Cloneable接口;多線程問題也需要考慮;
- 要實現clone方法的類,都應該實現Cloneable接口,同時把clone方法可見性設為public,返回類型為自己,應該調用super.clone()來創建對象,然后手動設置每個域的值
- clone方法太過復雜,如果不實現Cloneable接口,也可以通過別的方式實現copy功能,或者不提供copy功能,immutable提供copy功能是無意義的
- 提供拷貝構造函數,或者拷貝工廠方法,而且此種方法更加推薦,但也有其不足
- 設計用來被繼承的類時,如果不實現一個正確高效的clone重寫,那么其子類也將無法實現正確高效的clone功能
- Item 12: 當對象自然有序時,實現Comparable接口
- 實現Comparable接口可以利用其有序性特點,提高集合使用/搜索/排序的性能
- Contact
- sgn(x.compareTo(y)) == - sgn(y.compareTo(x)),當類型不對時,應該拋出ClassCastException,拋出異常的行為應該是一致的
- transitive: x.compareTo(y) > 0 && y.compareTo(z) > 0 ==> x.compareTo(z) > 0
- x.compareTo(y) == 0 ==> sgn(x.compareTo(z)) == sgn(y.compareTo(z))
- 建議,但非必須:與equals保持一致,即 x.compareTo(y) == 0 ==> x.equals(y),如果不一致,需要在文檔中明確指出
- TreeSet, TreeMap等使用的就是有序保存,而HashSet, HashMap則是通過equals + hashCode保存
- 當要為一個實現了Comparable接口的類增加成員變量時,不要通過繼承來實現,而是使用組合,并提供原有對象的訪問方法,以保持對Contract的遵循
- 實現細節
- 優先比較重要的域
- 謹慎使用返回差值的方式,有可能會溢出
Classes and Interfaces
- Item 13: 最小化類、成員的可見性
- 封裝(隱藏):公開的接口需要暴露,而接口的實現則需要隱藏,使得接口與實現解耦,降低模塊耦合度,增加可測試性、穩定性、可維護性、可優化性、可修改性
- 如果一個類只對一個類可見,則應該將其定義為私有的內部類,而沒必要public的類都應該定義為package private
- 為了便于測試,可以適當放松可見性,但也只應該改為package private,不能更高
- 成員不能是非private的,尤其是可變的對象。一旦外部可訪問,將失去對其內容的控制能力,而且會有多線程問題
- 暴露的常量也不能是可變的對象,否則public static final也將失去其意義,final成員無法改變其指向,但其指向的對象卻是可變的(immutable的對象除外),長度非0的數組同樣也是有問題的,可以考慮每次訪問時創建拷貝,或者使用
Collections.unmodifiableList(Arrays.asList(arr))
- Item 14: public class中,使用accessor method而非public field
- 后者外部可以直接訪問,失去了安全性
- package private或者private則可以不必這樣
- 把immutable的field置為public勉強可以接受,mutable的成員一定不能置為public
- Item 15: 最小化可變性
- 不提供可以改變本對象狀態的方法
- 保證類不可被繼承
- 使用final field
- 使用private field
- 在構造函數、accessor中,對mutable field使用defensive copy
- 實現建議
- 操作函數,例如BigInteger的add方法,不是static的,但也不能改變本對象的狀態,則使用functional的方式,返回一個新的對象,其狀態是本對象修改之后的狀態
- 如此實現的immutable對象生來就是線程安全的,無需同步操作,但應該鼓勵共用實例,避免創建過多重復的對象
- 正確實現的immutable對象也不需要clone, copy方法;可以適當引入Object cache;
- 劣勢
- 每一個值都需要一個對象,調用改變狀態的方法而創建一個新的對象,尤其是它是重量級的,開銷會變大;連續調用這樣的方法,影響更大;
- 為常用的多次操作組合提供一個方法
- 其他
- 保證class無法被繼承,除了聲明為final外,還可以將默認構造函數聲明為private或package private,然后提供public static工廠方法
- 使用public static工廠方法,具體實現類可以有多個,還能進行object cache
- 當實現Serializable接口是,一定要實現readObject/readResolve方法,或者使用ObjectOutputStream.writeUnshared/ObjectInputStream.readUnshared
- 小結
- 除非有很好的理由讓一個Class mutable,否則應該使其immutable
- 如果非要mutable,也應盡可能限制其可變性
- Item 16: Favor composition (and forwarding) over inheritance
- 跨包繼承、繼承不是被設計為應該被繼承的實現類,是一件很危險的事情,繼承接口、繼承抽象類,當然是沒問題的
- 如果子類的功能依賴于父類的實現細節,那么一旦父類發生變化,子類將有可能出現Bug,即便代碼都沒有修改;而設計為應被繼承的類,在修改后,是應該有文檔說明的,子類開發者既可以得知,也可以知道如何修改
- 例子:統計HashSet添加元素的次數
- 用繼承方式,重寫add,addAll,在其中計數,這就不對,因為HashSet內部的addAll是通過調用add實現的
- 但是通過不重寫addAll也只不對的,以后有可能HashSet的實現就變了
- 在重寫中重新實現一遍父類的邏輯也是行不通的,因為這可能會導致性能問題、bug等,而且有些功能不訪問私有成員也是無法實現的
- 還有一個原因就是父類的實現中,可能會增加方法,改變其行為,而這一點,在子類中是無法控制的
- 而通過組合的方式,將不會有這些問題,把另一個類的對象聲明為私有成員,外部將無法訪問它,自己也能在轉發(forwarding)過程中執行攔截操作,也不必依賴其實現細節,這種組合、轉發的實現被稱為wrapper,或者Decorator pattern,或者delegation(嚴格來說不是代理,代理一般wrapper對象都需要把自己傳入到被wrap的對象方法中?)
- 缺點
- 不適用于callback frameworks?
- 繼承應該在is-a的場景中使用
- 繼承除了會繼承父類的API功能,也會繼承父類的設計缺陷,而組合則可以隱藏成員類的設計缺陷
- Item 17: Design and document for inheritance or else prohibit it
- 一個類必須在文檔中說明,每個可重寫的方法,在該類的實現中的哪些地方會被調用(the class must document its self-use of overridable methods)。調用時機、順序、結果產生的影響,包括多線程、初始化等情況。
- 被繼承類應該通過謹慎選擇protected的方法或成員,來提供一些hook,用于改變其內部的行為,例如java.util.AbstractList::removeRange。
- The only way to test a class designed for inheritance is to write subclasses. 用于判斷是否需要增加或者減少protected成員/方法,通常寫3個子類就差不多了。
- You must test your class by writing subclasses before you release it.
- Constructors must not invoke overridable methods. 父類的構造函數比子類的構造函數先執行,而如果父類構造函數中調用了可重寫的方法,那么就會導致子類的重寫方法比子類的構造函數先執行,會導致corruption。
- 如果實現了Serializable/Cloneable接口,neither clone nor readObject may invoke an overridable method, directly or indirectly. 重寫方法會在deserialized/fix the clone’s state之前執行。
- 如果實現了Serializable接口,readResolve/writeReplace必須是protected,而非private
- designing a class for inheritance places substantial limitations on the class.
- The best solution to this problem is to prohibit subclassing in classes that are not designed and documented to be safely subclassed. 聲明為final class或者把構造函數私有化(提供public static工廠方法)。
- 如果確實想要允許繼承,就應該為每個被自己使用的可重寫方法都寫好文檔
- Item 18: Prefer interfaces to abstract classes
- Java類只允許單繼承,接口可以多繼承,使用接口定義類型,使得class hierarchy更加靈活
- 定義mixin(optional functionality to be "mixed in")時使用interface是很方便的,需要增加此功能的類只需要implement該接口即可,而如果使用抽象類,則無法增加一個extends語句
- 接口允許構建沒有hierarchy的類型系統
- 使用接口定義類型,可以使得item 16中提到的wrapper模式更加安全、強大,
- skeletal implementation:該類為abstract,把必須由client實現的方法設為abstract,可以有默認實現的則提供默認實現
- simulated multiple inheritance:通過實現定義的接口,同時在內部實現一個匿名的skeletal implementation,將對對該接口的調用轉發到匿名類中,起到“多繼承”的效果
- simple implementation:提供一個非抽象的接口實現類,提供一個最簡單、能work的實現,也允許被繼承
- 使用接口定義類型的缺點:不便于演進,一旦接口發布,如果想要增加功能(增加方法),則client將無法編譯;而使用abstract class,則沒有此問題,只需要提供默認實現即可
- 小結
- 通過接口定義類型,可以允許多實現(多繼承)
- 但是演進需求大于靈活性、功能性時,抽象類更合適
- 提供接口時,提供一個skeletal implementation,同時審慎考慮接口設計
- Item 19: 僅僅用interface去定義一個類型,該接口應該有實現類,使用者通過接口引用,去調用接口的方法
- 避免用接口去定義常量,應該用noninstantiable utility class去定義常量
- 相關常量的命名,通過公共前綴來實現分組
- Item 20: Prefer class hierarchies to tagged classes
- tagged class: 在內部定義一個tag變量,由其控制功能的轉換
- tag classes are verbose, error-prone, and inefficient
- 而class hierarchy,不同功能由不同子類實現,公共部分抽象為一個基類,也能反映出各個子類之間的關系
- Item 21: Use function objects to represent strategies
- 只提供一個功能函數的類實例,沒有成員變量,只需一個對象(單例),為其功能定義一個接口,則可以實現策略模式,把具體策略傳入相應函數中,使用策略
- 具體的策略實例通常使用匿名類定義,調用使用該策略的方法時才予以創建/預先創建好之后每次將其傳入
- Item 22: Favor static member classes over nonstatic
- 有4種nested class:non-static member class; static member class(inner class); anonymous class; local class
- static member class
- 經常作為helper class,和外部類一起使用
- 如果nested class的生命周期獨立于外部類存在,則必須定義為static member class,否則可能造成內存泄漏
- private static member class用處一:表示(封裝)外部類的一些成員,例如Map的Entry內部類。
- non-static member class
- 將持有外部類實例的強引用,可以直接引用外部類的成員和方法
- 用處一:定義一個Adapter,使得外部內的實例,可以作為和外部類語義不同的實例來查看(訪問),例如Collection的Iterator。
- 如果nested class不需要引用外部類的成員和方法,則一定要將其定義為static,避免空間/時間開銷,避免內存泄漏
- anonymous class
- 當在非static代碼塊內定義時,會持有外部類的引用,否則不會持有
- 限制
- 只能在被聲明的地方進行實例化
- 無法進行instanceof測試
- 不能用匿名類實現多個接口
- 不能用匿名類繼承一個類的同時實現接口
- 匿名類中新添加的方法無法在匿名類外部訪問
- 不能有static成員
- 應該盡量保持簡短
- 用處一:創建function object
- 用處二:創建process object,例如:Runnable, Thread, TimberTask
- 用處三:用于public static工廠方法,例如Collections類里面的一些工廠方法,很多是返回一個匿名的內部實現
- local class
- 比較少用
- 是否static取決于其定義的上下文
- 可以在作用域內重復使用
- 不能有static成員
- 也應盡量保持簡短
- 小結
- 四種nested class
- 如果nested class在整個外部類內都需要可見,或者定義代碼太長,應使用member class
- 能static就一定要static,即便需要對外部類進行引用,對于生命周期獨立于外部類的,也應該通過WeakReference進行引用,避免內存泄漏;至于生命周期和外部類一致的,則不必這樣
Generics
- Item 23: Don’t use raw types in new code
- Java泛型,例如
List<E>
,真正使用的時候都是List<String>
等,把E替換為實際的類型 - Java泛型從1.5引入,為了保持兼容性,實現的是偽泛型,類型參數信息在編譯完成之后都會被擦除,其在運行時的類型都是raw type,類型參數保存的都是Object類型,
List<E>
的raw type就是List
- 編譯器在編譯期通過類型參數,為讀操作自動進行了類型強制轉換,同時在寫操作時自動進行了類型檢查
- 如果使用raw type,那編譯器就不會在寫操作時進行類型檢查了,寫入錯誤的類型也不會報編譯錯誤,那么在后續讀操作進行強制類型轉換時,將會導致轉換失敗,拋出異常
- 一旦錯誤發生,應該讓它盡早被知道(拋出/捕獲),編譯期顯然優于運行期
-
List
與List<Object>
的區別 - 前者不具備類型安全性,后者具備,例如以下代碼
```java
// Uses raw type (List) - fails at runtime!
public static void main(String[] args) {
List<String> strings = new ArrayList<String>();
unsafeAdd(strings, new Integer(42));
String s = strings.get(0); // Compiler-generated cast
}
private static void unsafeAdd(List list, Object o) {
list.add(o);
}
```
不會報編譯錯誤,但會給一個編譯警告:`Test.java:10: warning: unchecked call to add(E) in raw type List list.add(o);`,而運行時則會發生錯誤。
+ 但如果使用`List<Object>`,即`unsageAdd`參數改為`List<Object> list, Object o`,則會報編譯錯誤:`Test.java:5: unsafeAdd(List<Object>,Object) cannot be applied to (List<String>,Integer) unsafeAdd(strings, new Integer(42));`
+ 因為`List<String>`是`List`的子類,但卻不是`List<Object>`的子類。
+ 并不是說這個場景應該使用`List<Object>`,這個場景應該使用`List<String>`,這里只是為了說明`List`和`List<Object>`是有區別的。
-
List
v.s.List<?>
(unbounded wildcard types),當不確定類型參數,或者說類型參數不重要時,也不應該使用raw type,而應該使用List<?>
- 任何參數化的List均是
List<?>
的子類,可以作為參數傳入接受List<?>
的函數,例如以下代碼均是合法的:
```java
void func(List<?> list) {
...
}
func(new List<Object>());
func(new List<Integer>());
func(new List<String>());
```
+ 持有`List<?>`的引用后,并不能向其中加入任何元素,讀取出來的元素也是`Object`類型,而不會被自動強轉為任何類型。
+ 如果`List<?>`的行為不能滿足需求,可以考慮使用模板方法,或者`List<E extends XXX>`(bounded wildcard types)
- You must use raw types in class literals.
-
List.class
,String[].class
, andint.class
are all legal, butList<String>.class
andList<?>.class
are not. -
instanceof
不支持泛型,以下用法是推薦的,但不應該將o
強轉為List
```java
// Legitimate use of raw type - instanceof operator
if (o instanceof Set) { // Raw type
Set<?> m = (Set<?>) o; // Wildcard type
...
}
```
- 相關術語匯總
java_generic_terms.png - Item 24: Eliminate unchecked warnings
- 當出現類型不安全的強制轉換時(一般都是涉及泛型,raw type),編譯器會給出警告,首先要做的是盡量消除不安全的轉換,消除警告
- 實在無法消除/確定不會導致運行時的
ClassCastException
,可以通過@SuppressWarnings("unchecked")
消除警告,但不要直接忽略該警告 - 使用
@SuppressWarnings("unchecked")
時,應該在注視內證明確實不存在運行時的ClassCastException
;同時應該盡量減小其作用的范圍,通常是應該為一個賦值語句添加注解 - Item 25: Prefer lists to arrays
- arrays are covariant(協變): 如果
Sub
是Super
的子類,那么Sub[]
也是Super[]
的子類 - generics are invariant(不變): 任意兩個不同的類
Type1
和Type2
,List<Type1>
和List<Type2>
之間沒有任何繼承關系 - 考慮以下代碼
// Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // Throws ArrayStoreException
// Won't compile!
List<Object> ol = new ArrayList<Long>(); // Incompatible types
ol.add("I don't fit in");
- arrays are reified(具體化): array在運行時能知道且強制要求元素的類型
- generics are implemented by erasure(non-reifiable): 僅僅在編譯時知道元素的類型
- 數組和泛型同時使用時會受到很大限制
- 以下語句均不能通過編譯:
new List<E>[], new List<String>[], new E[]
;但是聲明是可以的,例如List<String>[] stringLists
- non-reifiable type: 例如
E, List<E>, List<String>
,這些類型在運行時的信息比編譯時的信息更少 - 只有unbounded wildcard type才是reifiable的,如:
List<?>, Map<?, ?>
- 常規來說,不能返回泛型元素的數組,因為會報編譯錯誤:
generic array creation errors
- 當泛型和
varargs
一起使用時,也會導致編譯警告 - 有時為了類型安全,不得不做些妥協,犧牲性能和簡潔,使用List而不是數組
- 把數組強轉為non-reifiable類型是非常危險的,僅應在非常確定類型安全的情況下使用
- Item 26: Favor generic types
- 當需要一個類成員的數據類型具備一般性時,應該用泛型,這也正是泛型的設計場景之一,不應該用Object類
- 但使用泛型有時也不得不進行cast,例如當泛型遇上數組
- 總的來說把suppress數組類型強轉的unchecked warning比suppress一個標量類型強轉的unchecked warning風險更大,但有時出于代碼簡潔性考慮,也不得不做出妥協
- 有時看似與item 25矛盾,實屬無奈,Java原生沒有List,ArrayList不得不基于數組實現,HashMap也是基于數組實現的
- 泛型比使用者進行cast更加安全,而且由于Java泛型的擦除實現,也可以和未做泛型的老代碼無縫兼容
- Item 27: Favor generic methods
- 泛型方法的類型參數在函數修飾符(可見性/static/final等)和返回值之間,例子:
// Generic method
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
- recursive type bound
// Using a recursive type bound to express mutual comparability
public static <T extends Comparable<T>> T max(List<T> list) {...}
- 泛型方法要比方法使用者進行cast更加安全
- Item 28: Use bounded wildcards to increase API flexibility
- 考慮以下代碼
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
public void pushAll(Iterable<E> src);
public void popAll(Collection<E> dst);
}
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = ... ;
numberStack.pushAll(integers);
Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ... ;
numberStack.popAll(objects);
pushAll和popAll的調用均無法通過編譯,因為盡管Integer
是Number
的子類,但Iterable<Integer>
不是Iterable<Number>
的子類,這是由泛型的invariant特性導致的,所以Iterable<Integer>
不能傳入接受Iterable<Number>
參數的函數,popAll的使用同理
- bounded wildcards:
<? extends E>
,<? super E>
, PECS stands for producer-extends, consumer-super. 如果傳入的參數是要輸入給該類型數據的,則應該使用extends,如果是要容納該類型數據的輸出,則應該使用super - 這很好理解,作為輸入是要賦值給E類型的,當然應該是E的子類(這里的extends包括E類型本身);而容納輸出是要把E賦值給傳入參數的,當然應該是E的父類(同樣包括E本身)
- 返回值類型不要使用bounded wildcards,否則使用者也需要使用,這將會給使用者造成麻煩
- 代碼對于bounded wildcards的使用在使用者那邊應該是透明的,即他們不會感知到bounded wildcards的存在,如果他們也需要考慮bounded wildcards的問題,則說明對bounded wildcards的使用有問題了
- 有時候編譯器的類型推導在遇到bounded wildcards會無法完成,這時就需要顯示指定類型信息,例如:
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2);
Set<Integer> integers = ... ;
Set<Double> doubles = ... ;
//Set<Number> numbers = union(integers, doubles); //compile error
Set<Number> numbers = Union.<Number>union(integers, doubles); //compile pass
- Comparables are always consumers, so you should always use
Comparable<? super T>
in preference toComparable<T>
. The same is true of comparators, so you should always useComparator<? super T>
in preference toComparator<T>
. - unbounded type parameter(
<E> ... List<E>
) v.s. unbounded wildcard(List<?>
):if a type parameter appears only once in a method declaration, replace it with a wildcard. - Item 29: Consider typesafe heterogeneous containers
- 使用泛型時,類型參數是有限個的,例如
List<T>
,Map<K, V>
,但有時可能需要一個容器,能放入任意類型的對象,但需要具備類型安全性,例如數據庫的一行,它的每一列都可能是任意類型的數據 - 由于
Class
類從1.5就被泛型化了,所以使得這種需求可以實現,例如:
// Typesafe heterogeneous container pattern - API
public class Favorites {
public <T> void putFavorite(Class<T> type, T instance);
public <T> T getFavorite(Class<T> type);
}
- 通常這樣使用的
Class
對象被稱為type token,它傳入函數,用來表述編譯時和運行時的類型信息 -
Favorites
的實現也是很簡單的:
// Typesafe heterogeneous container pattern - implementation
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));
}
}
- 注意,這里的unbound wildcard并不是應用于Map的,而是應用于Class的類型參數,因此Map可以put key進去,而且key可以是任意類型參數的Class對象
- 另外,Map的value類型是Object,一旦put到Map中去,其編譯期類型信息就丟失了,將通過get方法的動態類型轉換(cast)來重新獲得其類型信息
- cast方法將檢查類型信息,如果是該類型(或其子類),轉換將成功,并返回引用,否則將拋出ClassCastException
- 這一heterogeneous container實現有兩個不足
- 通過為put方法傳入Class的raw type,使用者可以很輕易地破壞類型安全性,解決方案也很簡單,在put時也進行一下cast:
```java
// Achieving runtime type safety with a dynamic cast
public <T> void putFavorite(Class<T> type, T instance) {
favorites.put(type, type.cast(instance));
}
```
這樣做的效果是使得想要破壞類型安全性的put使用者產生異常,而使用get的使用者則不會因為惡意put使用者產生異常。這種做法也被`java.util.Collections`包中的一些方法使用,例如命名為checkedSet, checkedList, checkedMap的類。
+ 這個容器內不能放入non-reifiable的類型,例如`List<String>`,因為`List<String>.class`是有語法錯誤的,`List<String>`, `List<Integer>`都只有同一個class對象:`List.class`;另外`String[].class`是合法的。
-
Favorites
使用的類型參數是unbounded的,可以put任意類型,也可以使用bounded type token,使用bounded時可能需要把Class<?>
轉換為Class<? extends Annotation>
,直接用class.cast
將會導致unchecked warning,可以通過class.asSubclass
來進行轉換,例子:
// Use of asSubclass to safely cast to a bounded type token
static Annotation getAnnotation(AnnotatedElement element, String annotationTypeName) {
Class<?> annotationType = null; // Unbounded type token
try {
annotationType = Class.forName(annotationTypeName);
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
return element.getAnnotation(annotationType.asSubclass(Annotation.class));
}
摘錄來源:https://notes.piasy.com/Android-Java/EffectiveJava.html