本文部分摘自 On Java 8
概述
在 Java5 以前,普通的類和方法只能使用特定的型別:基本資料型別或類型別,如果編寫的程式碼需要應用於多種型別,這種嚴苛的限制對程式碼的束縛就會很大
Java5 的一個重大變化就是引入泛型,泛型實現了引數化型別,使得你編寫的元件(通常是集合)可以適用於多種型別。泛型的初衷是通過解耦類或方法與所使用的型別之間的約束,使得類或方法具備最寬泛的表達力。然而很快你就會發現,Java 中的泛型並沒有你想的那麼完美,甚至存在一些令人迷惑的實現
泛型類
促成泛型出現的最主要動機之一就是為了建立集合類,集合用於存放要使用到的物件。現有一個只能持有單個物件的類:
class Automobile {}
public class Holder1 {
private Automobile a;
public Holder1(Automobile a) { this.a = a; }
Automobile get() { return a; }
}
如果沒有泛型,那麼就必須明確指定其持有的物件的型別,會導致該複用性不高,它無法持有其他型別的物件,我們當然不希望為每個型別都編寫一個新類
在 Java5 以前,為了解決這個問題,我們可以讓這個類直接持有 Object 型別的物件,這樣就可以持有多種不同型別的物件了。但通常而言,我們只會用集合儲存同一型別的物件。泛型的主要目的之一就是用來約定集合要儲存什麼型別的物件,並且通過編譯器確保規約得以滿足
所以,與其使用 Object,我們更希望先指定一個型別佔位符,稍後再決定具體使用什麼型別。由此我們需要使用型別引數,用尖括號括住,放在類名後面。然後在使用這個類時,再用實際的型別替換此型別引數
public class GenericHolder<T> {
private T a;
public GenericHolder() {}
public void set(T a) { this.a = a; }
public T get() { return a; }
public static void main(String[] args) {
// 在 Java7 中右邊的尖括號可以為空
GenericHolder<Automobile> h2 = new GenericHolder<Automobile>();
GenericHolder<Automobile> h3 = new GenericHolder<>();
h3.set(new Automobile()); // 此處有型別校驗
Automobile a = h3.get(); // 無需型別轉換
//- h3.set("Not an Automobile"); // 報錯
}
}
元組類庫
有時一個方法需要能返回多個物件,而 return語句只能返回單個物件,解決的方法就是建立一個物件,用它來打包想要返回的多個物件。元組的概念正是基於此,元組將一組物件直接打包儲存於單一物件中,可以從該物件讀取其中元素,卻不允許向其中儲存新物件(這個概念也稱資料傳輸物件或信使)
元組可以具有任意長度,元組中的物件可以是不同型別的,我們希望能為每個物件指明型別,這時泛型就派上用場了。例如下面是一個可以儲存兩個物件的元組:
public class Tuple<A, B> {
public final A a1;
public final B a2;
public Tuple(A a, B b) { a1 = a; a2 = b; }
public String rep() { return a1 + ", " + a2; }
@Override
public String toString() {
return "(" + rep() + ")";
}
}
使用 final 修飾成員變數可以保證其不被修改,如果使用者想儲存不同的元素,那麼就必須建立新的 Tuple 物件。當然也可以允許使用者重新對 a1、a2 賦值,但無疑前一種形式會更加安全
利用繼承機制可以實現長度更長的元組:
public class Tuple3<A, B, C> extends Tuple2<A, B> {
public final C a3;
public Tuple3(A a, B b, C c) {
super(a, b);
a3 = c;
}
@Override
public String rep() {
return super.rep() + ", " + a3;
}
}
泛型方法
到目前為止,我們已經研究了引數化整個類,其實還可以引數化類中的方法。類本身是否是泛型,與它的方法是否是泛型並沒有什麼直接關係。我們應該儘可能使用泛型方法,通常將單個方法泛型化要比將整個類泛型化要更加清晰易懂
要定義泛型方法,請將泛型引數列表放置在返回值之前:
public class GenericMethods {
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
public static void main(String[] args) {
GenericMethods gm = new GenericMethods();
gm.f("");
gm.f(1);
gm.f(1.0);
gm.f(1.0F);
gm.f('c');
gm.f(gm);
}
}
使用泛型方法時,通常不需要指定引數型別,因為編譯器會找出這些型別,這稱為型別引數推斷,因此,對 f() 的呼叫看起來像普通的方法呼叫,而且像是被過載了無數次一樣
泛型擦除
當你開始深入研究泛型時,你會發現一個殘酷的現實:在泛型程式碼內部,無法獲取任何有關泛型引數型別的資訊
class Frob {}
class Fnorkle {}
class Quark<Q> {}
class Particle<POSITION, MOMENTUM> {}
public class LostInformation {
public static void main(String[] args) {
List<Frob> list = new ArrayList<>();
Map<Frob, Fnorkle> map = new HashMap<>();
Quark<Fnorkle> quark = new Quark<>();
Particle<Long, Double> p = new Particle<>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
System.out.println(Arrays.toString(p.getClass().getTypeParameters()));
}
}
/* Output:
[E]
[K,V]
[Q]
[POSITION,MOMENTUM]
*/
正如上例中輸出所示,你只能看到用作引數佔位符的識別符號,這並非有用的資訊。Java 泛型是使用擦除實現的,這意味著當你在使用泛型時,任何具體的型別資訊都被擦除了,你唯一知道的就是你在使用一個物件。因此 List<String> 和 List 在執行時實際上是相同的型別,它們都被擦除成原生型別 List
再來看一個例子:
class Manipulator<T> {
private T obj;
Manipulator(T x) {
obj = x;
}
// Error: cannot find symbol: method f():
public void manipulate() {
obj.f();
}
}
public class Manipulation {
public static void main(String[] args) {
HasF hf = new HasF();
Manipulator<HasF> manipulator = new Manipulator<>(hf);
manipulator.manipulate();
}
}
因為擦除,Java 編譯器無法將 manipulate() 方法能呼叫 obj 的 f() 方法這一需求對映到 HasF 具有 f() 方法這個事實上。為了呼叫 f(),我們必須協助泛型類,為泛型類給定一個邊界,以此告訴編譯器只能接受遵循這個邊界的型別。這裡重用了 extends 關鍵字。由於有了邊界,下面的程式碼就能通過編譯:
public class Manipulator2<T extends HasF> {
private T obj;
Manipulator2(T x) {
obj = x;
}
public void manipulate() {
obj.f();
}
}
邊界 <T extends HasF> 宣告 T 必須是 HasF 型別或其子類。如果情況確實如此,就可以安全地在 obj 上呼叫 f() 方法。泛型型別引數會擦除到它的第一個邊界(可能有多個邊界,稍後你將看到)。我們還提到了型別引數的擦除。編譯器實際上會把型別引數替換為它的擦除,就像上面的示例,T 擦除到了 HasF,就像在類的宣告中用 HasF 替換了 T 一樣。如果我們願意,完全可以把上例的 T 替換成 HashF,效果也是一樣的,那麼泛型的意義又何在呢?
這提出了很重要的一點:泛型只有在型別引數比某個具體型別(以及其子類)更加“泛化”,程式碼能跨多個類工作時才有用。因此,使用型別引數通常比簡單的宣告類更加複雜。但是,不能因此認為使用 <T extends HasF> 形式就是有缺陷的。你必須檢視所有的程式碼,從而確定程式碼是否複雜到必須使用泛型的程度
有關泛型擦除的困惑,其實是 Java 為實現泛型的一種妥協,因為泛型並不是 Java 語言出現時就有的。擦除減少了泛型的泛化性,泛型型別只有在靜態型別檢測期間才出現,在此之後,程式中的所有泛型型別都將被擦除,替換為它們的非泛型上界。例如, List<T> 這樣的型別註解會被擦除為 List,普通的型別變數在未指定邊界的情況下會被擦除為 Object
在 Java5 以前編寫的類庫是沒有使用泛型的,而作者可能打算重新用泛型編寫,或者根本不打算這樣做。Java 設計者們既要保證舊程式碼和類檔案依然合法,還得考慮當某個類庫變為泛型時,不會破壞依賴於它的程式碼和應用。Java 設計者們最終認為泛型是唯一可行的解決方案,擦除使得向泛型的遷移成為可能,為了實現非泛型的程式碼和泛型程式碼共存,必須將某個類庫使用了泛型這樣的“證據”擦除
基於上述觀點,當你在編寫泛型程式碼時,必須時刻提醒自己,你只是看起來擁有有關引數的型別資訊而言。因為擦除,我們無法在執行時知道確切的型別,為了補償擦除帶來的弊端,我們可以為所需的型別顯示傳遞一個 Class 物件,以在型別表示式中使用它
class Building {
}
class House extends Building {
}
public class ClassTypeCapture<T> {
Class<T> kind;
public ClassTypeCapture(Class<T> kind) {
this.kind = kind;
}
public boolean f(Object arg) {
return kind.isInstance(arg);
}
public static void main(String[] args) {
ClassTypeCapture<Building> ctt1 =
new ClassTypeCapture<>(Building.class);
System.out.println(ctt1.f(new Building()));
System.out.println(ctt1.f(new House()));
ClassTypeCapture<House> ctt2 =
new ClassTypeCapture<>(House.class);
System.out.println(ctt2.f(new Building()));
System.out.println(ctt2.f(new House()));
}
}
邊界和萬用字元
由於擦除會刪除型別資訊,因此唯一可用於無限制泛型引數的方法是那些 Object 可用的方法。邊界允許我們對泛型使用的引數型別施以型別,將引數限制為某型別的子集,那麼就可以呼叫該子集中的方法。為了應用約束,Java 泛型使用了 extends 關鍵字
class Coord {
public int x, y, z;
}
interface Weight {
int weight();
}
class Solid<T extends Coord & Weight> {
T item;
Solid(T item) {
this.item = item;
}
T getItem() {
return item;
}
int getX() {
return item.x;
}
int getY() {
return item.y;
}
int getZ() {
return item.z;
}
int weight() {
return item.weight();
}
}
class Bounded
extends Coord implements Weight {
@Override
public int weight() {
return 0;
}
}
public class BasicBounds {
public static void main(String[] args) {
Solid<Bounded> solid =
new Solid<>(new Bounded());
solid.getY();
solid.weight();
}
}
引入萬用字元可以在泛型例項化時更加靈活地控制,也可以在方法中控制方法的引數,具體語法如下:
- ? extends T:表示 T 或 T 的子類
- ? super T:表示 T 或 T 的父類
- ?:表示可以是任意型別
值得注意的問題
在這裡主要闡述在使用 Java 泛型時會出現的各類問題
1. 任何基本資料型別不能作為型別引數
Java 泛型的限制之一是不能將基本型別用作型別引數。因此,不能建立 ArrayList<int> 之類的東西。 解決方法是使用基本型別的包裝器類以及自動裝箱機制。如果建立一個 ArrayList<Integer>,並將基本型別 int 應用於這個集合,那麼你將發現自動裝箱機制將自動地實現 int 到 Integer 的雙向轉換,這幾乎就像是有一個 ArrayList<int> 一樣
2. 實現引數化介面
一個類不能實現同一個泛型介面的兩種變體,由於擦除的原因,這兩個變體會成為相同的介面。下面是產生這種衝突的情況:
interface Payable<T> {}
class Employee implements Payable<Employee> {}
class Hourly extends Employee implements Payable<Hourly> {}
Hourly 不能編譯,因為擦除會將 Payable<Employe> 和 Payable<Hourly> 簡化為相同的類 Payable,這樣,上面的程式碼就意味著在重複兩次地實現相同的介面。十分有趣的是,如果從 Payable 的兩種用法中都移除掉泛型引數(就像編譯器在擦除階段所做的那樣)這段程式碼就可以編譯
3. 轉型和警告
使用帶有泛型型別引數的轉型不會有任何效果,例如:
class Storage<T> {
private Object obj;
Storage() {
obj = new Object();
}
@SuppressWarnings("unchecked")
public T pop() {
return (T)obj;
}
}
public class GenericCast {
public static void main(String[] args) {
Storage<String> storage = new Storage<>();
System.out.println(storage.pop());
}
}
如果沒有 @SuppressWarnings 註解,編譯器將對 pop() 產生 “unchecked cast” 警告。由於擦除的原因,編譯器無法知道這個轉型是否是安全的,並且 pop() 方法實際上並沒有執行任何轉型。 這是因為,T 被擦除到它的第一個邊界,預設情況下是 Object,因此 pop() 實際上只是將 Object 轉型為 Object
4. 過載
下面的程式是不能編譯的,因為擦除,所以過載方法產生了相同的型別簽名
public class UseList<W, T> {
void f(List<T> v) {}
void f(List<W> v) {}
}