天天看點

徹底講清 Java 的泛型(上)1 與 C++ 的比較2 簡單泛型3 一個元組類庫泛型方法 簡化元組

普通的類和方法隻能使用特定的類型:基本資料類型或類類型。

如果編寫的代碼需要應用于多種類型,這種嚴苛的限制對代碼的束縛就會很大。

多态是一種面向對象思想的泛化機制。可以将方法的參數類型設為基類,這樣的方法就可以接受任何派生類作為參數,包括暫時還不存在的類。

這樣的方法更通用,應用範圍更廣。在類内部也是如此,在任何使用特定類型的地方,基類意味着更大的靈活性。

除了 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 的泛型(上)1 與 C++ 的比較2 簡單泛型3 一個元組類庫泛型方法 簡化元組

可複用性不高,無法持有其他類型的對象。不希望為碰到的每個類型都編寫一個新的類。

Java 5 前,可以讓這個類

直接持有

Object

對象

  • 一個

    ObjectHolder

    先後持有了三種不同類型的對象:
  • 徹底講清 Java 的泛型(上)1 與 C++ 的比較2 簡單泛型3 一個元組類庫泛型方法 簡化元組
  • 現在,ObjectHolder 可以持有任何類型的對象

通常隻會用集合存儲同一種類型的對象。

泛型的主要目的之一:約定集合要存儲什麼類型對象,并且通過編譯器保證

是以與其使用 Object ,我們更希望先指定一個類型占位符,稍後決定具體使用什麼類型。

要達到這個目的,需要使用類型參數,用尖括号包覆,放在類名後面。

然後在使用類時,再用實際類型替換此類型參數。

在下面的例子中,T 就是類型參數:

徹底講清 Java 的泛型(上)1 與 C++ 的比較2 簡單泛型3 一個元組類庫泛型方法 簡化元組

建立 GenericHolder 對象時,必須指明要持有的對象的類型,置于尖括号

然後,就隻能在 GenericHolder 中存儲該類型(或其子類,多态與泛型不沖突)的對象。

當你調用 get() 取值時,直接就是正确的類型。

這就是Java 泛型的核心概念:你隻需告訴編譯器要使用什麼類型,剩下的細節交給它來處理。

h3 的定義非常繁複。在 = 左邊有 GenericHolder<Automobile>, 右邊又重複了一次。在 Java 5 中,這種寫法被解釋成“必要的”,Java 7 修正了這個問題。

一般來說,你可以認為泛型和其他類型差不多,隻不過它們碰巧有類型參數。

在使用泛型時,隻需要指定它們的名稱和類型參數清單。

3 一個元組類庫

有時一個方法需要能傳回多個對象。而 return 語句隻能傳回單個對象,解決方法就是建立一個對象,用它打包想要傳回的多個對象。

當然,可以在每次需要的時候,專門建立一個類來完成這樣的工作。

有了泛型,我們就可以一勞永逸。同時,還獲得了編譯時的類型安全。

這稱為

元組

将一組對象直接打包存儲于單一對象中。可以從該對象讀取其中的元素,但不允許向其中存儲新對象(這個概念也稱為 資料傳輸對象 或 信使 )。

元組可以具有任意長度,元組中對象可以不同類型。

不過,我們希望能夠為每個對象指明類型,并且從元組中讀取出來時,能夠得到正确的類型。

要處理不同長度的問題,我們需要建立多個不同的元組。

下面是一個可以存儲兩個對象的元組:

徹底講清 Java 的泛型(上)1 與 C++ 的比較2 簡單泛型3 一個元組類庫泛型方法 簡化元組

構造函數傳入要存儲的對象。這個元組隐式地保持了其中元素的次序。

初次閱讀你可能認為這違反了 Java 程式設計的封裝原則:a1 和 a2 應該聲明為 private,然後提供 getFirst() 和 getSecond() 取值方法

這樣做能提供的“安全性”:元組的使用程式可以讀取 a1 和 a2 對它們執行任何操作,但無法對 a1 和 a2 重新指派。final 可以實作同樣效果,更簡潔。

而這裡是另一種設計思路:

允許使用者給 a1 和 a2 重新指派。然而更加安全,如果使用者想存儲不同的元素,就會強制他們建立新的 Tuple2 對象。

我們可以利用繼承機制實作長度更長的元組。添加更多的類型參數:

徹底講清 Java 的泛型(上)1 與 C++ 的比較2 簡單泛型3 一個元組類庫泛型方法 簡化元組

示範需要,再定義兩個類:

// generics/Amphibian.java
public class Amphibian {}

// generics/Vehicle.java
public class Vehicle {}      

使用元組時,隻需要定義一個長度适合的元組,将其作為傳回值即可

徹底講清 Java 的泛型(上)1 與 C++ 的比較2 簡單泛型3 一個元組類庫泛型方法 簡化元組

有了泛型很容易地建立元組,令其傳回一組任意類型的對象。

通過

ttsi.a1 = "there"

語句的報錯,我們可以看出,final 聲明确實可以確定 public 字段在對象被構造出來之後就不能重新指派了。

new

表達式有些啰嗦。

泛型方法 簡化元組

使用類型參數推斷和靜态導入,把早期的元組重寫為更通用的庫。

重載靜态方法建立元組:

徹底講清 Java 的泛型(上)1 與 C++ 的比較2 簡單泛型3 一個元組類庫泛型方法 簡化元組

我們修改 TupleTest.java 來測試 Tuple.java :

徹底講清 Java 的泛型(上)1 與 C++ 的比較2 簡單泛型3 一個元組類庫泛型方法 簡化元組

f() 傳回參數化 Tuple2, f2() 傳回未參數化的 Tuple2。編譯器不會在這裡警告 f2() ,因為傳回值未以參數化方式使用。從某種意義上說,它被“向上轉型”為一個未參數化的 Tuple2 。 但是,如果嘗試将 f2() 的結果放入到參數化的 Tuple2 中,則編譯器将發出警告。