普通的類和方法只能使用特定的類型:基本數據類型或類類型。
如果編寫的代碼需要應用於多種類型,這種嚴苛的限制對代碼的束縛就會很大。
-
多態是一種面向對象思想的泛化機制。可以將方法的參數類型設爲基類,這樣的方法就可以接受任何派生類作爲參數,包括暫時還不存在的類。
這樣的方法更通用,應用範圍更廣。在類內部也是如此,在任何使用特定類型的地方,基類意味着更大的靈活性。
除了final
類(或只提供私有構造函數的類)任何類型都可被擴展,所以大部分時候這種靈活性是自帶的。 -
接口可以突破繼承體系的限制
單一的繼承體系太過侷限,因爲只有繼承體系中的對象才能適用基類作爲參數的方法中。如果方法以接口而不是類作爲參數,限制就寬鬆多了,只要實現了接口就可以。這給予調用方一種選項,通過調整現有的類來實現接口,滿足方法參數要求。 -
接口的限制
一旦指定了接口,它就要求你的代碼必須使用特定的接口。而我們希望編寫更通用的代碼,能夠適用“非特定的類型”,而不是一個具體的接口或類。
這就是泛型的概念,是 Java 5 的重大變化。泛型實現了參數化類型,這樣你編寫的組件(比如集合)可以適用於多種類型。“泛型”這個術語的含義是“適用於很多類型”。
編程語言中泛型出現的初衷是通過解耦類或方法與所使用的類型之間的約束,使得類或方法具備最寬泛的表達力。
隨後你會發現 Java 中泛型的實現並沒有那麼“泛”,你可能會質疑“泛型”這個詞是否合適用來描述這一功能。
實例化一個類型參數時,編譯器會負責轉型並確保類型的正確性。使用別人創建好的泛型相對容易,但是創建自己的泛型時,就會遇到很多意料之外的麻煩。
在很多情況下,它可以使代碼更直接更優雅。不過,如果你見識過那種實現了更純粹的泛型的編程語言,那麼,Java 可能會令你失望。
本章會介紹 Java 泛型的優點與侷限。我會解釋 Java 的泛型是如何發展成現在這樣的,希望能夠幫助你更有效地使用這個特性。[^1]
1 與 C++ 的比較
Java 的設計者曾說過,這門語言的靈感主要來自 C++ 。儘管如此,學習 Java 時基本不用參考 C++ 。
但是,Java 中的泛型需要與 C++ 進行對比,理由有兩個
1.1 理解 C++ 模板
泛型的主要靈感來源,包括基本語法的某些特性,有助於理解泛型的基礎理念。
同時可以理解
- Java 泛型的侷限是什麼
- 爲什麼會有這些侷限
- 最終明確 Java 泛型的邊界
只有知道了某個技術不能做什麼,你才能更好地做到所能做的(不必浪費時間在死衚衕)。
1.2 誤解 C++ 模板
在 Java 社區中,大家普遍對 C++ 模板有一種誤解,而這種誤解可能會令你在理解泛型的意圖時產生偏差。
因此,本章中會介紹少量 C++ 模板的例子,僅當它們確實可以加深理解時纔會引入。
2 簡單泛型
促成泛型出現的最主要的動機之一是創建集合類:幾乎所有程序在運行過程中都會涉及到一組對象
持有單個對象的類
明確指定其持有的對象的類型
可複用性不高,無法持有其他類型的對象。不希望爲碰到的每個類型都編寫一個新的類。
Java 5 前,可以讓這個類
直接持有 Object
對象
- 一個
ObjectHolder
先後持有了三種不同類型的對象:
現在,ObjectHolder
可以持有任何類型的對象
通常只會用集合存儲同一種類型的對象。
泛型的主要目的之一:約定集合要存儲什麼類型對象,並且通過編譯器保證
因此與其使用 Object
,我們更希望先指定一個類型佔位符,稍後決定具體使用什麼類型。
要達到這個目的,需要使用類型參數,用尖括號括住,放在類名後面。
然後在使用類時,再用實際類型替換此類型參數。
在下面的例子中,T
就是類型參數:
創建 GenericHolder
對象時,必須指明要持有的對象的類型,置於尖括號
然後,就只能在 GenericHolder
中存儲該類型(或其子類,多態與泛型不衝突)的對象。
當你調用 get()
取值時,直接就是正確的類型。
這就是Java 泛型的核心概念:你只需告訴編譯器要使用什麼類型,剩下的細節交給它來處理。
h3
的定義非常繁複。在 =
左邊有 GenericHolder<Automobile>
, 右邊又重複了一次。在 Java 5 中,這種寫法被解釋成“必要的”,Java 7 修正了這個問題。
一般來說,你可以認爲泛型和其他類型差不多,只不過它們碰巧有類型參數。
在使用泛型時,只需要指定它們的名稱和類型參數列表。
3 一個元組類庫
有時一個方法需要能返回多個對象。而 return 語句只能返回單個對象,解決方法就是創建一個對象,用它打包想要返回的多個對象。
當然,可以在每次需要的時候,專門創建一個類來完成這樣的工作。
有了泛型,我們就可以一勞永逸。同時,還獲得了編譯時的類型安全。
這稱爲
元組
將一組對象直接打包存儲於單一對象中。可以從該對象讀取其中的元素,但不允許向其中存儲新對象(這個概念也稱爲 數據傳輸對象 或 信使 )。
元組可以具有任意長度,元組中對象可以不同類型。
不過,我們希望能夠爲每個對象指明類型,並且從元組中讀取出來時,能夠得到正確的類型。
要處理不同長度的問題,我們需要創建多個不同的元組。
下面是一個可以存儲兩個對象的元組:
構造函數傳入要存儲的對象。這個元組隱式地保持了其中元素的次序。
初次閱讀你可能認爲這違反了 Java 編程的封裝原則:a1
和 a2
應該聲明爲 private,然後提供 getFirst()
和 getSecond()
取值方法
這樣做能提供的“安全性”:元組的使用程序可以讀取 a1
和 a2
對它們執行任何操作,但無法對 a1
和 a2
重新賦值。final
可以實現同樣效果,更簡潔。
- 而這裏是另一種設計思路:
允許用戶給a1
和a2
重新賦值。然而更加安全,如果用戶想存儲不同的元素,就會強制他們創建新的Tuple2
對象。
我們可以利用繼承機制實現長度更長的元組。添加更多的類型參數:
演示需要,再定義兩個類:
// generics/Amphibian.java
public class Amphibian {}
// generics/Vehicle.java
public class Vehicle {}
使用元組時,只需要定義一個長度適合的元組,將其作爲返回值即可
有了泛型很容易地創建元組,令其返回一組任意類型的對象。
通過 ttsi.a1 = "there"
語句的報錯,我們可以看出,final 聲明確實可以確保 public 字段在對象被構造出來之後就不能重新賦值了。
new
表達式有些囉嗦。稍後會介紹利用 泛型方法 簡化