泛型是我們需要的程式設計手段,使用泛型機制撰寫的程式代碼要比那些雜亂地使用 Object 變數,然后再進行強制型別轉換的代碼具有更好的安全性和可讀性,
至少在表面上看來,泛型很像 C++ 中的模板,與 Java —樣,在 C++ 中,模板也是最先被添加到語言中支持強型別集合的,但是,多年之后人們發現模板還有其他的用武之地,學習完本章的內容可以發現 Java 中的泛型在程式中也有新的用途,
為什么要使用泛型程式設計
泛型程式設計(Generic programming)意味著撰寫的代碼可以被很多不同型別的物件所重用,例如,我們并不希望為聚集 String 和 File 物件分別設計不同的類,實際上,也不需要這樣做,因為一個 ArrayList 類可以聚集任何型別的物件,這是一個泛型程式設計的實體,
實際上,在 Java 增加泛型類之前已經有一個 ArrayList 類,下面來研究泛型程式設計的機制是如何演變的,另外還會講解這對于用戶和實作者來說意味著什么,
型別引數的好處
在 Java 中增加范型類之前,泛型程式設計是用繼承實作的,實作時使用通用型別(如 Object 或 Comparable 介面),在使用時進行強制型別轉換,
泛型對于集合類尤其有用,ArrayList 就是一個無處不在的集合類,ArrayList 類維護一個 Object 型別的陣列(Object 類是所有類的父類):
// before generic classes
public class ArrayList {
private Object[] elementData;
// ...
public Object get(int i) { ... }
public void add(Object o) { ... }
}
這種方法有兩個問題,
- 當獲取一個值時,必須進行強制型別轉換,
- 此外,這里沒有錯誤檢査,可以向陣列串列中添加任何類的物件,對于 files.add(new File("..."); 這個呼叫,編譯和運行都不會出錯,然而在其他地方,如果將 get() 的結果強制型別轉換為 String 型別, 就會產生一個錯誤,
ArrayList files = new ArrayList();
String filename = (String) files.get(0);
泛型提供了一個更好的解決方案:型別引數(type parameters),ArrayList 類有一個型別引數用來指示元素的型別:ArrayList<String> files = new ArrayList<String>();
這使得代碼具有更好的可讀性,人們一看就知道這個陣列串列中包含的是 String 物件,
在 Java7 及以后的版本中,建構式中可以省略泛型型別:ArrayList<String> files = new ArrayList<>();
省略的型別可以從變數的型別推斷得出,
編譯器也可以很好地利用這個資訊,當呼叫 get() 方法的時候,不需要進行強制型別轉換,編譯器就知道回傳值型別為 String,而不是 Object:String filename = files.get(0);
編譯器還知道 ArrayList
files,add(new File("...")); // can only add String objects to an ArrayList<String>
型別引數的魅力在于:使得程式具有更好的可讀性和安全性,
誰想成為泛型程式員
使用像 ArrayList 這樣的泛型類很容易,大多數 Java 程式員都使用 ArrayList
但是,實作一個泛型類并沒有那么容易,對于型別引數,使用這段代碼的程式員可能想要內置(plugin)所有的類,他們希望在沒有過多的限制以及混亂的錯誤訊息的狀態下,做所有的事情,因此,一個泛型程式員的任務就是預測出所用類的未來可能有的所有用途,
這一任務難到什么程度呢?下面是標準類別庫的設計者們肯定產生爭議的一個典型問題,ArrayList 類的 addAll() 方法用來添加另一個集合的全部元素,程式員可能想要將 ArrayList
泛型程式設計劃分為 3 個能力級別,基本級別是,僅僅使用泛型類:典型的是像 ArrayList 這樣的集合,不必考慮它們的作業方式與原因,大多數應用程式員將會停留在這一級別上,直到出現了什么問題,當把不同的泛型類混合在一起時,或是在與對型別引數一無所知的遺留的代碼進行銜接時,可能會看到含混不清的錯誤訊息,如果這樣的話,就需要系統地學習 Java 泛型來解決這些問題,而不要胡亂地猜測,當然,最終可能想要實作自己的泛型類與泛型方法,
應用程式員很可能不喜歡撰寫太多的泛型代碼,JDK 開發人員已經做出了很大的努力,為所有的集合類提供了型別引數,憑經驗來說,那些原本涉及許多來自通用型別(如 Object 或 Comparable 介面)的強制型別轉換的代碼一定會因使用型別引數而受益,
本章介紹實作自己的泛型代碼需要了解的各種知識,希望大多數讀者可以利用這些知識解決一些疑難問題,并滿足對于引數化集合類的內部作業方式的好奇心,
泛型類
泛型類(generic class)就是具有一個或多個型別引數的類,
本章使用一個簡單的 Pair 類作為例子,
public class Pair<T> {
private T first;
private T second;
public Pair() { first = null ; second = null ; }
public Pair(T first, T second) { this.first = first; this.second = second; }
public T getFirst() { return first; }
public T getSecond() { return second; }
public void setFirst(T newValue) { first = newValue; }
public void setSecond(T newValue) { second = newValue; }
}
Pair 類引入了一個型別引數 T,用尖括號(< >)括起來,并放在類名的后面,
泛型類可以有多個型別引數,如果有多個型別變數,多個型別變數之間用 “,”逗號分隔,例如,可以定義 Pair 類,其中第一個域和第二個域使用不同的型別:public class Pair<T, U> { ... }
類定義中的型別引數指定方法的回傳型別以及域和區域變數的型別,例如,private T first;
用具體的型別替換型別引數就可以實體化泛型型別,例如:Pair
- Pair
() - Pair
(String, String)
和方法:
- String getFirst()、String getSecond()
- void setFirst(String)、void setSecond(String)
換句話說,泛型類可看作普通類的工廠,
泛型方法
前面已經介紹了如何定義一個泛型類,實際上,還可以定義一個帶有型別引數的簡單方法,
class ArrayAlg {
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
}
這個方法是在普通類中定義的,而不是在泛型類中定義的,然而,這是一個泛型方法,可以從尖括號和型別引數看出這一點,注意,型別引數放在修飾符(這里是 public static)的后面,回傳型別的前面,
泛型方法可以定義在普通類中,也可以定義在泛型類中,
當呼叫一個泛型方法時,在方法名前的尖括號中放入具體的型別:
String middle = ArrayAlg.<String>getMiddle("]ohn", "Q.", "Public");
在這種情況(實際也是大多數情況)下,方法呼叫中可以省略
String middle = ArrayAlg.getMiddle("]ohn", "Q.", "Public");
型別引數的限定
有時,類或方法需要對型別引數加以約束,下面是一個典型的例子,我們要計算陣列中的最小元素:
class ArrayAIg {
// almost correct
public static <T> T min(T[] a) {
if (a == null || a.length == 0) {
return null;
}
T smallest = a[0];
for (int i = 1; i < a.length; i++) {
if (smallest.compareTo(a[i]) > 0) smallest = a[i];
}
return smallest;
}
}
但是,這里有一個問題,請看一下 min() 方法的代碼內部,變數 smallest 型別為 T,這意味著它可以是任何一個類的物件,怎么才能確信 T 所屬的類有 compareTo() 方法呢?
解決這個問題的方案是將 T 限制為實作了 Comparable 介面(只含一個 compareTo() 方法的標準介面)的類,可以通過對型別引數 T 設定限定(bound)實作這一點:
public static <T extends Comparable> T min(T[] a) {}
現在,泛型的 min() 方法只能被實作了 Comparable 介面的類(如 String、LocalDate 等)的陣列呼叫,
T extends 系結型別
表示 T 應該是系結型別的子型別(subtype),T 和系結型別可以是類,也可以是介面,
一個型別引數或通配符可以有多個限定,多個限定之間用 “ &” 分隔,例如:T extends Comparable & Serializable,在Java的限定中,可以根據需要擁有多個介面限定,但至多有一個類限定,如果用一個類作為限定,它必須放在限定串列中的第一個位置,
// ok
T extends Object & Comparable & Serializable
// error
T extends Comparable & Serializable & Object
泛型代碼和虛擬機
型別擦除
型別擦除是Java泛型實作的一種方式,
型別擦除指的是:在編譯時,將泛型型別擦除成其原始型別,
虛擬機沒有泛型型別物件,所有物件都屬于普通類,無論何時定義一個泛型型別,都自動提供了一個相應的原始型別(raw type),型別引數用第一個限定的型別來替換,如果沒有給定限定就用 Object 替換,例如:
- 類 Pair
中的型別引數沒有顯式的限定,因此,原始型別用 Object 替換 T, - 類 Interval<T extends Comparable & Serializable> 中第一個限定的型別為 Comparable,因此,原始型別用 Comparable 替換 T,
Pair
public class Pair {
private Object first;
private Object second;
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() { return first; }
public Object getSecond() { return second; }
public void setFirst(Object newValue) { first = newValue; }
public void setSecond(Object newValue) { second = newValue; }
}
翻譯泛型運算式
當程式呼叫泛型方法時,如果擦除回傳型別,編譯器插入強制型別轉換,例如,下面這個陳述句序列:
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
擦除 getFirst() 方法的回傳型別后,getFirst() 方法將回傳 Object 型別,編譯器自動插入 Employee 的強制型別轉換,也就是說,編譯器把這個方法呼叫翻譯為兩條虛擬機指令:
- 對 Pair#getFirst() 原始方法的呼叫,
- 將回傳的 Object 型別強制轉換為 Employee 型別,
當存取一個泛型域時也要插入強制型別轉換,假設 Pair 類的 first 域和 second 域都是公有的,運算式:Employee buddy = buddies.first;
也會在結果位元組碼中插入強制型別轉換,
翻譯泛型方法
型別擦除也會出現在泛型方法中,程式員通常認為下述的泛型方法 public static <T extends Comparable> T min(T[] a)
是一個完整的方法族,而擦除型別之后,只剩下一個方法:public static Comparable min(Comparable[] a)
注意,型別引數 T 已經被擦除了,只留下了限定型別 Comparable,
型別擦除帶來了兩個復雜問題,看一看下面這個示例:
class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
if (second.compareTo(getFirst()) >= 0) {
super.setSecond(second);
}
}
...
}
一個日期區間是一對 LocalDate 物件,并且需要覆寫這個方法來確保第二個值永遠不小于第一個值,這個類擦除后變成
// after erasure
class DateInterval extends Pair {
public void setSecond(LocalDate second) { ... }
...
}
令人感到奇怪的是,存在另一個從 Pair 繼承的 setSecond() 方法,即 public void setSecond(Object second)
這顯然是一個不同的方法,因為它有一個不同型別的引數 Object,而不是 LocalDate,然而,不應該不一樣,考慮下面的陳述句序列:
DateInterval interval = new DateInterval(...);
Pair<LocalDate> pair = interval; // OK assignment to superclass
pair.setSecond(aDate);
這里,希望對 setSecond() 的呼叫具有多型性,并呼叫最合適的那個方法,由于 pair 參考 Datelnterval 物件,所以應該呼叫 Datelnterval.setSecond(),問題在于型別擦除與多型發生了沖突,要解決這個問題,就需要編譯器在 Datelnterval 類中生成一個橋方法(bridge method):
public void setSecond(Object second) { setSecond((Date) second); }
有關泛型的事實
需要記住有關 Java 泛型轉換的事實:
- 虛擬機中沒有泛型,只有普通的類和方法,
- 所有的型別引數都用它們的限定型別替換,
- 橋方法被合成來保持多型,
- 為保持型別安全性,必要時插入強制型別轉換,
類 A 是類 B 的子類,但是 G 和 G 不具有繼承關系,二者是并列關系,
public static void printBuddies(Pair<Employee> p) { ... }
// Manager 類是 Employee 類的子類
Pair<Manager> pair = new Pair<>();
// error(固定的泛型型別系統的局限,通配符型別解決了這個問題)
printBuddies(pair);
泛型一般有三種使用方式:泛型類、泛型方法、泛型介面,
// 泛型類
public class Pair<T>
// 實體化泛型類
Pair<String> pair = new Pair<>();
// 繼承泛型類,指定型別
class DateInterval extends Pair<LocalDate>
// 泛型方法
public static <T> T getMiddle(T... a)
// 泛型介面
public interface Generator<T>
// 實作泛型介面,指定型別
class GeneratorImpl implements Generator<String>
通配符型別
固定的泛型型別系統使用起來并沒有那么令人愉快,型別系統的研究人員知道這一點已經有一段時間了,Java 的設計者發明了一種巧妙的(仍然是安全的)“解決方案”:通配符型別,下面幾小節會介紹如何處理通配符,
通配符概念
通配符型別中,允許型別引數變化,例如,通配符型別 Pair<? extends Employee> 表示任何泛型 Pair 型別,它的型別引數是 Employee 的子類,如 Pair
假設要撰寫一個列印雇員對的方法,像這樣:
public static void printBuddies(Pair<Employee> p) {
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName() + " and " + second.getName() + " are buddies.");
}
正如前面講到的,不能將 Pair
public static void printBuddies(Pair<? extends Employee> p)
型別 Pair
使用通配符會通過 Pair<? extends Employee> 的參考破壞 Pair
Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair<? extends Employee> wildcardBuddies = managerBuddies; // OK
wildcardBuddies.setFirst(lowlyEmployee); // compile-time error
這可能不會引起破壞,對 setFirst() 的呼叫有一個型別錯誤,要了解其中的緣由,請仔細看一看型別 Pair<? extends Employee>,其方法似乎是這樣的:
? extends Employee getFirst()
void setFirst(? extends Employee)
這樣將不可能呼叫 setFirst() 方法,編譯器只知道需要某個 Employee 的子型別,但不知道具體是什么型別,它拒絕傳遞任何特定的型別,畢竟 ? 不能用來匹配,
使用 getFirst() 就不存在這個問題:將 getFirst() 的回傳值賦給一個 Employee 的參考完全合法,這就是引入有限定的通配符的關鍵之處,現在已經有辦法區分安全的訪問器方法和不安全的更改器方法了,
通配符的超型別限定
通配符限定與型別引數限定十分類似,但是,通配符限定還有一個附加的能力,即可以指定一個超型別限定(supertype bound),如下所示:? super Manager,
這個通配符限制為 Manager 的所有超型別,(已有的 super 關鍵字十分準確地描述了這種聯系)
參考資料
《Java核心技術卷一:基礎知識》(第10版)第 8 章:泛型程式設計
本文來自博客園,作者:真正的飛魚,轉載請注明原文鏈接:https://www.cnblogs.com/feiyu2/p/17476544.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/555049.html
標籤:Java
上一篇:【解決一個小問題】golang 的 `-race`選項導致 unsafe代碼 panic
下一篇:返回列表