Java 之路 (十五) -- 泛型上(泛型類、泛型方法、有界型別引數、泛型與繼承、型別推斷)
Thinking in Java 中關於泛型的講解篇幅實在過長,前後嘗試閱讀這一章,但總是覺得找不到要點,很迷。於是放棄 Thinking in Java 泛型一章的閱讀,轉而官方教程,本章可以算作官方教程的中文版。
1.為什麼使用泛型
簡單來說,泛型使型別在定義類、介面和方法時成為引數。就像在方法宣告中使用形式引數一樣,型別引數提供了一種使用不同輸入重用相同程式碼的方法。不同之處在於形式引數的輸入是值,而型別引數的輸入是型別。
使用泛型的程式碼比非泛型程式碼有許多好處:
在編譯時進行更強大的型別檢查。
Java編譯器將強型別檢查應用於通用程式碼,並在程式碼違反型別安全時發出錯誤。修復編譯時錯誤比修復執行時錯誤更容易,後者很難發現錯誤源頭。消除轉型
以下沒有泛型的程式碼片段需要強制轉換:
List list = new ArrayList(); list.add("hello"); String s = (String)list.get(0);
使用泛型時,不需要型別轉換:
List <String> list = new ArrayList<String>(); list.add("hello"); String s = list.get(0); //沒有轉型
使程式設計師能夠實現通用演算法。
通過使用泛型,程式設計師可以實現通用演算法,這些演算法可以處理不同型別的集合,可以自定義,並且型別安全且易於閱讀。
2. 泛型類
泛型類是對型別進行引數化的類或介面。 下面一步步展示該概念。
2.1 簡單的 Box 類
如果我們想在一個類中存放任何型別的物件,怎麼做呢?沒錯,使用 Object 即可。
下面展示一個可對任何類物件進行操作的非泛型 Box 類:
public class Box {
private Object object;
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}
由於它的方法接受或返回一個Object
Integer
,並期望從中獲取Integer
,而程式碼的另一部分可能會錯誤地傳入String
,從而導致執行時錯誤。
2.2 Box 類的泛型版本
上面提到,通過 Object 儲存,不存在任何型別資訊,這可能導致使用時型別錯誤。於是泛型發揮作用了。
泛型類定義格式如下:
class name<T1, T2, ..., Tn> { /* ... */ }
用尖括號將型別引數包起來,並跟在類名後面。
於是2.1中的程式碼修改之後如下:
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
如程式碼所示,所有 Object 都被 T 替換。型別變數可以是制定的任何非基本型別:類、介面、陣列或者其他型別變數。且型別變數 T 可以在類的任何位置使用。
同樣,也適用於將泛型應用於介面,如下:
interface Box<T> { /*...*/}
2.3 型別引數命名約定
按照慣例,型別引數名稱是單個大寫字母 。
最常用的型別引數(識別符號)名稱是:
- E - Element(Java Collections Framework廣泛使用)
- K - key
- N - number
- T - 類(型別)
- V - value
- S,U,V等 - 第2,第3,第4型別
2.4 呼叫和例項化泛型類
將 T 替換為某些具體類即可,例如 Integer:
Box<Integer> integerBox = new Box<Integer>();
//在 Java SE 7 及更高版本中,只要編譯期可以從上下文中確定或推斷型別引數,就可以用一組空的型別引數“<>” 替換呼叫泛型類的建構函式所需的型別引數
//如下:
Box<Integer> integerBox = new Box<>();
泛型類的呼叫通常稱為引數化型別
2.5 多種型別引數
泛型類可以有多個型別引數,如下展示一個通用的 OrderPair 類,實現了 Pair 介面:
public interface Pair<K, V> {
public K getKey();
public V getValue();
}
public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
以下語句建立兩個 OrderPair 類的例項:
Pair<String,Integer> p1 = new OrderedPair<String,Integer>("Even",8);
Pair<String,String> p2 = new OrderedPair <String,String>("hello","world");
//或如下
Pair<String,Integer> p1 = new OrderedPair<>("Even",8);
Pair<String,String> p2 = new OrderedPair <>("hello","world");
可以看到,分別將 K、V 例項化為 String、Integer 和 String、String,由於自動裝箱機制,這裡傳入的基本資料型別會自動包裝為其對應值的物件。
基本資料型別不能作為引數型別,之所以可以傳入基本型別引數,是因為自動裝箱機制會將其轉化為對應值的物件。
2.6 引數化型別
引數化型別(如 List<String>
)耶爾可以作為型別引數,如:
OrderedPair<String, Box<Integer>>> p = new OrderedPair<>("primes",new Box<Integer>(...));
2.7 “原生”型別
原生型別(Raw type)是沒有任何型別引數的泛型類/介面的名稱,即原生型別的概念只針對泛型而言。
例如,給定泛型 Box 類:
public class Box<T> {
public void set(T t) { /* ... */ }
// ...
}
在建立引數化型別的 Box<T>
,需要傳入實際型別引數,如:
Box <Integer> intBox = new Box<>();
但是,如果不指定型別引數,那麼則會建立一個原生型別 Box:
Box rawBox = new Box();
Box 是泛型 Box<T>
的原生型別。
換個更熟悉的例子,List<String>
的 原生型別是 List,即原生型別可以理解為去掉了泛型型別資訊。
原生型別主要存在於歷史遺留程式碼中(JDK 5.0 以前),因為許多類 在 JDK 5.0 以前是不支援泛型的,所以為了向後相容,令原生型別預設提供 Object,然後允許將引數化型別賦值給原始型別:
Box <String> stringBox = new Box<>();
Box rawBox = stringBox; // 這是沒問題的
但是當將原生型別賦值給引數化型別,或者原生型別呼叫泛型型別中定義的方形方法,都會收到警告:
Box rawBox = new Box(); // rawBox是Box<T>的原始型別
Box <Integer> intBox = rawBox; //warning: unchecked conversion
Box <String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8); //warning: unchecked invocation to set(T)
上述原生型別會繞過泛型型別檢查,這會導致捕獲不安全的程式碼推遲到執行時,因此應該避免使用原生型別。
3. 泛型方法
泛型方法是引入其自己的型別引數的方法。 這類似於宣告泛型型別,但型別引數的範圍僅限於宣告它的方法。 允許使用靜態和非靜態泛型方法,以及泛型類建構函式。
泛型方法的語法包括位於尖括號內部的型別引數列表,它置於方法返回型別之前。
[許可權修飾詞] <T1,T2,...,Tn> methods(/*...*/) {/*...*/}
下面舉個例子:
public class Util {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
Util 類包含一個泛型方法 compare,用以比較兩個 Pair 物件。
呼叫此方法的完整語法如下:
Pair <Integer,String> p1 = new Pair<>(1,"apple");
Pair <Integer,String> p2 = new Pair<>(2,"pear");
boolean same = Util.<Integer,String> compare(p1,p2);
由於已明確提供該型別,通常,可以省略引數型別,編譯期將推斷所需的型別:
Pair <Integer,String> p1 = new Pair<>(1,"apple");
Pair <Integer,String> p2 = new Pair<>(2,"pear");
boolean same = Util.compare(p1,p2);
此功能稱為型別推斷,允許將泛型方法作為普通方法來呼叫,而無需在尖括號之間指定型別。
4. 有界型別引數
有時,我們希望可以限制類型引數的型別。例如,對數字操作的方法可能只想接收 Number 或其子類的物件。這種情況下,有界型別引數就發揮作用了。
宣告有界型別引數,需要指定型別引數的名稱,然後是 extends 關鍵字,後接其上限。
<T extends SomeType>
注意此情景下的 extends 包含了通常意義的 extends(在類中) 和 implements(在介面中)
例子如下:
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public <U extends Number> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(new Integer(10));
integerBox.inspect("some text"); // error: this is still String!
}
}
這裡我們指定接收 Number 及其子型別物件,於是當我們想 inspect 方法傳遞一個 String 物件時,會發生錯誤。
上述限制類型只是有界型別引數的作用之一,其實潛在的更重要的功能時,有界型別引數允許我們呼叫邊界中定義的方法:
public class NaturalNumber<T extends Integer> {
private T n;
public NaturalNumber(T n) { this.n = n; }
public boolean isEven() {
return n.intValue() % 2 == 0;
}
// ...
}
isEven() 方法通過 n 呼叫 Integer 類中定義的 intValue 方法。
4.1 多個邊界
實際上型別引數可以有多個邊界:
<T extends B1 & B2 & B3>
具有多個邊界時,型別變數是指定的所有型別的子型別。
注意:邊界中必須將 類Class 放在 介面interface 之前,否則會出錯:
Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }
4.2 泛型方法與有界型別引數
有界型別引數往往是通用演算法實現的關鍵。考慮以下方法,該方法計算陣列 T[] 中大於指定元素 elem 的元素數量。
public static <T> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e > elem) // compiler error
++count;
return count;
}
看起來方法很簡單,但是編譯會失敗,這是因為 ”>“ 僅適用於基本型別,不能用與物件比較。
解決此方法,可以考慮使用由 Comparable<T>
介面限定的型別引數:
public interface Comparable<T> {
public int compareTo(T o);
}
修改後的程式碼如下:
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e.compareTo(elem) > 0)
++count;
return count;
}
說實話,這裡有些矇蔽。。
經搜尋後,
<T extends Comparable<T>>
這種寫法就是相當於<T>
,但是 T 要implements Comparable<T>
,所以如果傳入基本型別都是可以的,因為基本型別都是實現了Comparable<T>
介面的
5. 泛型與繼承
通常,只要型別相容(繼承),就可以將一個型別的物件轉換為另一個型別的物件。如下:
Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // Object 是 Integer 的父類
同時 Integer 也是一種 Number(面向物件中繼承表示 ”is-a“ 關係),所以下面程式碼也是有效的:
public void someMethod(Number n) { /* ... */ }
someMethod(new Integer(10)); // OK
someMethod(new Double(10.1)); // OK
泛型也是如此,可以執行泛型型別的呼叫,將 Number 作為其型別引數傳遞,如果引數與 Number 相容,則允許任何呼叫:
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
但是,世事無絕對。考慮以下方法:
public void boxTest(Box<Number> n) { /* ... */ }
**你可能會因為它可以接受一個型別為 Box<Number>
的引數,按照上面的結論,就以為向其傳遞 Box<Integer>
或者 Box<Double>
?這裡需要強調,後者是不能傳遞的。因為 Box<Integer>
和 Box<Double>
並不是 Box<Number>
的子型別。
注意:給定具體型別 A、B,MyClass<A>
和 MyClass<B>
無關,二者唯一的交集是公共父類為 Object。
5.1 泛型類和子型別
一個類或介面的型別引數與另一個類或介面的型別引數之間的關係由 extends 和 implements 子句確定。
舉個例子:Collection 類,ArrayList <E>
實現List <E>
, List <E>擴充套件Collection <E>
。 因此ArrayList <String>
是List <String>
的子型別,它是Collection <String>
的子型別。 只要不改變型別引數,就會在型別之間保留子型別關係。
此時,假設我們要定義自己的 List 介面 PayloadList
,它將 泛型 P 與每個元素綁在一起,它的宣告如下:
interface PayloadList<E,P> extends List<E> {
void setPayload(int index, P val);
...
}
此時PayloadList
的以下引數化是List <String>的
子型別:
PayloadList <字串,字串>
PayloadList <字串,整數>
PayloadList <字串,異常>
6. 型別推斷
型別推斷是 Java 編譯器檢視每個方法呼叫和相應宣告,根據型別引數(或引數)進行合適的方法呼叫;型別推斷會嘗試查詢適用於所有引數的最具體型別。
6.1 型別推斷與泛型方法
在 3.泛型方法 中介紹了型別推斷,它使得你能夠向呼叫普通方法一樣呼叫泛型方法,而無需在尖括號之間指定型別。
通常,Java 編譯期可以推斷泛型方法呼叫的型別引數,因此多數情況下,不必指定。
依舊是前面的例子,兩種呼叫方式:
官方教程中將完整寫法稱作 型別見證(type witness)
boolean same = Util.<Integer,String> compare(p1,p2);//指定型別
boolean same = Util.compare(p1,p2);//不指定型別,Java 編譯期會自動推斷型別引數是Integer 和 String
6.2 型別推斷和泛型類的例項化
在 2.4 呼叫和例項化泛型類中也提到過,只要編譯器能夠從殺昂下文中推斷出型別引數,就可以用一組空的型別引數(<>) 替換呼叫泛型類的建構函式所需的型別引數。
例如,對以下變數宣告:
Map<String, List<String>> myMap = new HashMap<String, List<String>>();
可以寫成:
Map<String, List<String>> myMap = new HashMap<>();
6.3 型別推斷 與 泛型/非泛型類的泛型構造方法
首先明確一點,泛型類和泛型方法沒什麼關係,一個類是不是泛型類與其中是否包含泛型方法無關。
建構函式在泛型和非泛型類中都可以是泛型的,換句話說,它們可以具有自己的型別引數:
class MyClass<X> {
<T> MyClass(T t) {
// ...
}
}
考慮 MyClass 類的例項化:
new MyClass<Integer>("");
該語句將泛型類的型別引數 X 指定為 Integer,泛型構造方法的型別引數T 指定為 String,因此實際上該建構函式的實際引數是 String 物件。
Java SE7之前,編譯期能夠推斷泛型構造引數的型別引數。Java SE7 之後,使用 <> 使編譯期推斷正在例項化的泛型類的型別引數:
MyClass<Integer> myObject = new MyClass<>("");
此例中,編譯期將 泛型類 MyClass<X>
的型別引數X 推斷為 Integer,同時推斷出泛型類的建構函式的型別引數 T 的型別為 String。
6.4 目標型別
Java編譯器利用目標型別來推斷泛型方法呼叫的型別引數。 表示式的目標型別是Java編譯器所期望的資料型別,具體取決於表示式的顯示位置。 考慮方法Collections.emptyList
,宣告如下:
static <T> List<T> emptyList();
對於以下賦值語句:
List<String> listOne = Collections.emptyList();
此語句建立 List<String>
的例項,此資料型別即為目標型別;而 emptyList() 方法返回 List<T>
型別物件,所以編譯期推斷型別引數 T 必須是 String。這適用於 Java SE 7以上。當然,我們可以用完整寫法:(這個場景下不是必須的)
List<String> listOne = Collections.<String>emptyList();
下面給出一個必須使用完整寫法(型別見證)的例子,考慮以下寫法:
void processStringList(List<String> stringList) {
// process stringList
}
如果我們需要向 processStringList 中傳入 emptyList() 方法作為引數,如以下語句:
processStringList(Collections.emptyList());
上述語句在Java SE 7 中編譯失敗,必須指定型別引數;但是在 Java SE 8 中可以編譯成功。