《Effective Java》筆記(上)

《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,那編譯器就不會在寫操作時進行類型檢查了,寫入錯誤的類型也不會報編譯錯誤,那么在后續讀操作進行強制類型轉換時,將會導致轉換失敗,拋出異常
  • 一旦錯誤發生,應該讓它盡早被知道(拋出/捕獲),編譯期顯然優于運行期
  • ListList<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, and int.class are all legal, but List<String>.class and List<?>.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(協變): 如果SubSuper的子類,那么Sub[]也是Super[]的子類
  • generics are invariant(不變): 任意兩個不同的類Type1Type2,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的調用均無法通過編譯,因為盡管IntegerNumber的子類,但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 to Comparable<T>. The same is true of comparators, so you should always use Comparator<? super T> in preference to Comparator<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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • 對象的創建與銷毀 Item 1: 使用static工廠方法,而不是構造函數創建對象:僅僅是創建對象的方法,并非Fa...
    孫小磊閱讀 2,010評論 0 3
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,781評論 18 139
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,717評論 18 399
  • 《Effective Java》筆記(下) Enums and Annotations Item 30: Use ...
    OCNYang閱讀 2,043評論 1 5
  • 積極的人像太陽,照到哪里哪里亮;消極的人像月亮,初一十五都一樣。 當你面對一個成功者,你會發現對方一般都很陽光、面...
    自由飛翔的風閱讀 306評論 8 4