本文部分摘自 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) {}
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/226387.html
標籤:其他
