Java泛型學習---第二篇
1.泛型之擦拭法
泛型是一種類似”模板程式碼“的技術,不同語言的泛型實現方式不一定相同。
Java語言的泛型實現方式是擦拭法(Type Erasure)。
所謂擦拭法是指,虛擬機器對泛型其實一無所知,所有的工作都是編譯器做的。
例如,我們編寫了一個泛型類Pair<T>
,這是編譯器看到的程式碼:
public class Pair<T> { private T first; private T last; public Pair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { return first; } public T getLast() { return last; } }
而虛擬機器根本不知道泛型。這是虛擬機器執行的程式碼:
public class Pair { private Object first; private Object last; public Pair(Object first, Object last) { this.first = first; this.last = last; } public Object getFirst() { return first; } public Object getLast() { return last; } }
因此,Java使用擦拭法實現泛型,導致了:
- 編譯器把型別
<T>
視為Object
; - 編譯器根據
<T>
實現安全的強制轉型。
使用泛型的時候,我們編寫的程式碼也是編譯器看到的程式碼:
Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();
而虛擬機器執行的程式碼並沒有泛型:
Pair p = new Pair("Hello", "world"); String first = (String) p.getFirst(); String last = (String) p.getLast();
所以,Java的泛型是由編譯器在編譯時實行的,編譯器內部永遠把所有型別T
視為Object
處理,但是,在需要轉型的時候,編譯器會根據T
的型別自動為我們實行安全地強制轉型。
瞭解了Java泛型的實現方式——擦拭法,我們就知道了Java泛型的侷限:
侷限一:<T>
不能是基本型別,例如int
,因為實際型別是Object
,Object
型別無法持有基本型別:
Pair<int> p = new Pair<>(1, 2); // compile error!
侷限二:無法取得帶泛型的Class
。觀察以下程式碼:
public class Main {
public static void main(String\[\] args) {
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
因為T
是Object
,我們對Pair<String>
和Pair<Integer>
型別獲取Class
時,獲取到的是同一個Class
,也就是Pair
類的Class
。
換句話說,所有泛型例項,無論T
的型別是什麼,getClass()
返回同一個Class
例項,因為編譯後它們全部都是Pair<Object>
。
侷限三:無法判斷帶泛型的型別:
Pair<Integer> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>) {
}
原因和前面一樣,並不存在Pair<String>.class
,而是隻有唯一的Pair.class
。
侷限四:不能例項化T
型別:
public class Pair<T> {
private T first;
private T last;
public Pair() {
// Compile error:
first = new T();
last = new T();
}
}
上述程式碼無法通過編譯,因為構造方法的兩行語句:
first = new T();
last = new T();
擦拭後實際上變成了:
first = new Object();
last = new Object();
這樣一來,建立new Pair<String>()
和建立new Pair<Integer>()
就全部成了Object
,顯然編譯器要阻止這種型別不對的程式碼。
要例項化T
型別,我們必須藉助額外的Class<T>
引數:
public class Pair<T> {
private T first;
private T last;
public Pair(Class<T> clazz) {
first = clazz.newInstance();
last = clazz.newInstance();
}
}
上述程式碼藉助Class<T>
引數並通過反射來例項化T
型別,使用的時候,也必須傳入Class<T>
。例如:
Pair<String> pair = new Pair<>(String.class);
因為傳入了Class<String>
的例項,所以我們藉助String.class
就可以例項化String
型別。
不恰當的覆寫方法
有些時候,一個看似正確定義的方法會無法通過編譯。例如:
public class Pair<T> {
public boolean equals(T t) {
return this == t;
}
}
這是因為,定義的equals(T t)
方法實際上會被擦拭成equals(Object t)
,而這個方法是繼承自Object
的,編譯器會阻止一個實際上會變成覆寫的泛型方法定義。
換個方法名,避開與Object.equals(Object)
的衝突就可以成功編譯:
public class Pair<T> {
public boolean same(T t) {
return this == t;
}
}
泛型繼承
一個類可以繼承自一個泛型類。例如:父類的型別是Pair<Integer>
,子類的型別是IntPair
,可以這麼繼承:
public class IntPair extends Pair<Integer> {
}
使用的時候,因為子類IntPair
並沒有泛型型別,所以,正常使用即可:
IntPair ip = new IntPair(1, 2);
前面講了,我們無法獲取Pair<T>
的T
型別,即給定一個變數Pair<Integer> p
,無法從p
中獲取到Integer
型別。
但是,在父類是泛型型別的情況下,編譯器就必須把型別T
(對IntPair
來說,也就是Integer
型別)儲存到子類的class檔案中,不然編譯器就不知道IntPair
只能存取Integer
這種型別。
在繼承了泛型型別的情況下,子類可以獲取父類的泛型型別。例如:IntPair
可以獲取到父類的泛型型別Integer
。獲取父類的泛型型別程式碼比較複雜:
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
public class Main {
public static void main(String\[\] args) {
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
class IntPair extends Pair<Integer> {
public IntPair(Integer first, Integer last) {
super(first, last);
}
}
因為Java引入了泛型,所以,只用Class
來標識型別已經不夠了。實際上,Java的型別系統結構如下:
┌────┐
│Type│
└────┘
▲
│
┌────────────┬────────┴─────────┬───────────────┐
│ │ │ │
┌─────┐┌─────────────────┐┌────────────────┐┌────────────┐
│Class││ParameterizedType││GenericArrayType││WildcardType│
└─────┘└─────────────────┘└────────────────┘└────────────┘
小結
Java的泛型是採用擦拭法實現的;
擦拭法決定了泛型<T>
:
- 不能是基本型別,例如:
int
; - 不能獲取帶泛型型別的
Class
,例如:Pair<String>.class
; - 不能判斷帶泛型型別的型別,例如:
x instanceof Pair<String>
; - 不能例項化
T
型別,例如:new T()
。
泛型方法要防止重複定義方法,例如:public boolean equals(T obj)
;
子類可以獲取父類的泛型型別<T>
。
2.extends萬用字元
我們前面已經講到了泛型的繼承關係:Pair<Integer>
不是Pair<Number>
的子類。
假設我們定義了Pair<T>
:
public class Pair<T> { ... }
然後,我們又針對Pair<Number>
型別寫了一個靜態方法,它接收的引數型別是Pair<Number>
:
public class PairHelper {
static int add(Pair<Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
}
上述程式碼是可以正常編譯的。使用的時候,我們傳入:
int sum = PairHelper.add(new Pair<Number>(1, 2));
注意:傳入的型別是Pair<Number>
,實際引數型別是(Integer, Integer)
。
既然實際引數是Integer
型別,試試傳入Pair<Integer>
:
public class Main {
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
直接執行,會得到一個編譯錯誤:
incompatible types: Pair<Integer> cannot be converted to Pair<Number>
原因很明顯,因為Pair<Integer>
不是Pair<Number>
的子類,因此,add(Pair<Number>)
不接受引數型別Pair<Integer>
。
但是從add()
方法的程式碼可知,傳入Pair<Integer>
是完全符合內部程式碼的型別規範,因為語句:
Number first = p.getFirst();
Number last = p.getLast();
實際型別是Integer
,引用型別是Number
,沒有問題。問題在於方法引數型別定死了只能傳入Pair<Number>
。
有沒有辦法使得方法引數接受Pair<Integer>
?辦法是有的,這就是使用Pair<? extends Number>
使得方法接收所有泛型型別為Number
或Number
子類的Pair
型別。我們把程式碼改寫如下:
public class Main {
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
這樣一來,給方法傳入Pair<Integer>
型別時,它符合引數Pair<? extends Number>
型別。這種使用<? extends Number>
的泛型定義稱之為上界萬用字元(Upper Bounds Wildcards),即把泛型型別T
的上界限定在Number
了。
除了可以傳入Pair<Integer>
型別,我們還可以傳入Pair<Double>
型別,Pair<BigDecimal>
型別等等,因為Double
和BigDecimal
都是Number
的子類。
如果我們考察對Pair<? extends Number>
型別呼叫getFirst()
方法,實際的方法簽名變成了:
<? extends Number> getFirst();
即返回值是Number
或Number
的子類,因此,可以安全賦值給Number
型別的變數:
Number x = p.getFirst();
然後,我們不可預測實際型別就是Integer
,例如,下面的程式碼是無法通過編譯的:
Integer x = p.getFirst();
這是因為實際的返回型別可能是Integer
,也可能是Double
或者其他型別,編譯器只能確定型別一定是Number
的子類(包括Number
型別本身),但具體型別無法確定。
我們再來考察一下Pair<T>
的set
方法:
public class Main {
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
}
不出意外,我們會得到一個編譯錯誤:
incompatible types: Integer cannot be converted to CAP#1
where CAP#1 is a fresh type-variable:
CAP#1 extends Number from capture of ? extends Number
編譯錯誤發生在p.setFirst()
傳入的引數是Integer
型別。有些童鞋會問了,既然p
的定義是Pair<? extends Number>
,那麼setFirst(? extends Number)
為什麼不能傳入Integer
?
原因還在於擦拭法。如果我們傳入的p
是Pair<Double>
,顯然它滿足引數定義Pair<? extends Number>
,然而,Pair<Double>
的setFirst()
顯然無法接受Integer
型別。
這就是<? extends Number>
萬用字元的一個重要限制:方法引數簽名setFirst(? extends Number)
無法傳遞任何Number
的子型別給setFirst(? extends Number)
。
這裡唯一的例外是可以給方法引數傳入null
:
p.setFirst(null); // ok, 但是後面會丟擲NullPointerException
p.getFirst().intValue(); // NullPointerException
extends萬用字元的作用
如果我們考察Java標準庫的java.util.List<T>
介面,它實現的是一個類似“可變陣列”的列表,主要功能包括:
public interface List<T> {
int size(); // 獲取個數
T get(int index); // 根據索引獲取指定元素
void add(T t); // 新增一個新元素
void remove(T t); // 刪除一個已有元素
}
現在,讓我們定義一個方法來處理列表的每個元素:
int sumOfList(List<? extends Integer> list) {
int sum = 0;
for (int i=0; i<list.size(); i++) {
Integer n = list.get(i);
sum = sum + n;
}
return sum;
}
為什麼我們定義的方法引數型別是List<? extends Integer>
而不是List<Integer>
?從方法內部程式碼看,傳入List<? extends Integer>
或者List<Integer>
是完全一樣的,但是,注意到List<? extends Integer>
的限制:
- 允許呼叫
get()
方法獲取Integer
的引用; - 不允許呼叫
set(? extends Integer)
方法並傳入任何Integer
的引用(null
除外)。
因此,方法引數型別List<? extends Integer>
表明了該方法內部只會讀取List
的元素,不會修改List
的元素(因為無法呼叫add(? extends Integer)
、remove(? extends Integer)
這些方法。換句話說,這是一個對引數List<? extends Integer>
進行只讀的方法(惡意呼叫set(null)
除外)。
使用extends限定T型別
在定義泛型型別Pair<T>
的時候,也可以使用extends
萬用字元來限定T
的型別:
public class Pair<T extends Number> { ... }
現在,我們只能定義:
Pair<Number> p1 = null;
Pair<Integer> p2 = new Pair<>(1, 2);
Pair<Double> p3 = null;
因為Number
、Integer
和Double
都符合<T extends Number>
。
非Number
型別將無法通過編譯:
Pair<String> p1 = null; // compile error!
Pair<Object> p2 = null; // compile error!
因為String
、Object
都不符合<T extends Number>
,因為它們不是Number
型別或Number
的子類。
小結
使用類似<? extends Number>
萬用字元作為方法引數時表示:
-
方法內部可以呼叫獲取
Number
引用的方法,例如:Number n = obj.getFirst();
; -
方法內部無法呼叫傳入
Number
引用的方法(null
除外),例如:obj.setFirst(Number n);
。
即一句話總結:使用extends
萬用字元表示可以讀,不能寫。
使用類似<T extends Number>
定義泛型類時表示:
- 泛型型別限定為
Number
以及Number
的子類。
super萬用字元
我們前面已經講到了泛型的繼承關係:Pair<Integer>
不是Pair<Number>
的子類。
考察下面的set
方法:
void set(Pair<Integer> p, Integer first, Integer last) {
p.setFirst(first);
p.setLast(last);
}
傳入Pair<Integer>
是允許的,但是傳入Pair<Number>
是不允許的。
和extends
萬用字元相反,這次,我們希望接受Pair<Integer>
型別,以及Pair<Number>
、Pair<Object>
,因為Number
和Object
是Integer
的父類,setFirst(Number)
和setFirst(Object)
實際上允許接受Integer
型別。
我們使用super
萬用字元來改寫這個方法:
void set(Pair<? super Integer> p, Integer first, Integer last) {
p.setFirst(first);
p.setLast(last);
}
注意到Pair<? super Integer>
表示,方法引數接受所有泛型型別為Integer
或Integer
父類的Pair
型別。
下面的程式碼可以被正常編譯:
public class Main {
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
}
考察Pair<? super Integer>
的setFirst()
方法,它的方法簽名實際上是:
void setFirst(? super Integer);
因此,可以安全地傳入Integer
型別。
再考察Pair<? super Integer>
的getFirst()
方法,它的方法簽名實際上是:
? super Integer getFirst();
這裡注意到我們無法使用Integer
型別來接收getFirst()
的返回值,即下面的語句將無法通過編譯:
Integer x = p.getFirst();
因為如果傳入的實際型別是Pair<Number>
,編譯器無法將Number
型別轉型為Integer
。
注意:雖然Number
是一個抽象類,我們無法直接例項化它。但是,即便Number
不是抽象類,這裡仍然無法通過編譯。此外,傳入Pair<Object>
型別時,編譯器也無法將Object
型別轉型為Integer
。
唯一可以接收getFirst()
方法返回值的是Object
型別:
Object obj = p.getFirst();
因此,使用<? super Integer>
萬用字元表示:
-
允許呼叫
set(? super Integer)
方法傳入Integer
的引用; -
不允許呼叫
get()
方法獲得Integer
的引用。
唯一例外是可以獲取Object
的引用:Object o = p.getFirst()
。
換句話說,使用<? super Integer>
萬用字元作為方法引數,表示方法內部程式碼對於引數只能寫,不能讀。
對比extends和super萬用字元
我們再回顧一下extends
萬用字元。作為方法引數,<? extends T>
型別和<? super T>
型別的區別在於:
-
<? extends T>
允許呼叫讀方法T get()
獲取T
的引用,但不允許呼叫寫方法set(T)
傳入T
的引用(傳入null
除外); -
<? super T>
允許呼叫寫方法set(T)
傳入T
的引用,但不允許呼叫讀方法T get()
獲取T
的引用(獲取Object
除外)。
一個是允許讀不允許寫,另一個是允許寫不允許讀。
先記住上面的結論,我們來看Java標準庫的Collections
類定義的copy()
方法:
public class Collections {
// 把src的每個元素複製到dest中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i);
dest.add(t);
}
}
}
它的作用是把一個List
的每個元素依次新增到另一個List
中。它的第一個引數是List<? super T>
,表示目標List
,第二個引數List<? extends T>
,表示要複製的List
。我們可以簡單地用for
迴圈實現複製。在for
迴圈中,我們可以看到,對於型別<? extends T>
的變數src
,我們可以安全地獲取型別T
的引用,而對於型別<? super T>
的變數dest
,我們可以安全地傳入T
的引用。
這個copy()
方法的定義就完美地展示了extends
和super
的意圖:
-
copy()
方法內部不會讀取dest
,因為不能呼叫dest.get()
來獲取T
的引用; -
copy()
方法內部也不會修改src
,因為不能呼叫src.add(T)
。
這是由編譯器檢查來實現的。如果在方法程式碼中意外修改了src
,或者意外讀取了dest
,就會導致一個編譯錯誤:
public class Collections {
// 把src的每個元素複製到dest中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
...
T t = dest.get(0); // compile error!
src.add(t); // compile error!
}
}
這個copy()
方法的另一個好處是可以安全地把一個List<Integer>
新增到List<Number>
,但是無法反過來新增:
// copy List<Integer> to List<Number> ok:
List<Number> numList = ...;
List<Integer> intList = ...;
Collections.copy(numList, intList);
// ERROR: cannot copy List<Number> to List<Integer>:
Collections.copy(intList, numList);
而這些都是通過super
和extends
萬用字元,並由編譯器強制檢查來實現的。
PECS原則
何時使用extends
,何時使用super
?為了便於記憶,我們可以用PECS原則:Producer Extends Consumer Super。
即:如果需要返回T
,它是生產者(Producer),要使用extends
萬用字元;如果需要寫入T
,它是消費者(Consumer),要使用super
萬用字元。
還是以Collections
的copy()
方法為例:
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i=0; i<src.size(); i++) {
T t = src.get(i); // src是producer
dest.add(t); // dest是consumer
}
}
}
需要返回T
的src
是生產者,因此宣告為List<? extends T>
,需要寫入T
的dest
是消費者,因此宣告為List<? super T>
。
無限定萬用字元
我們已經討論了<? extends T>
和<? super T>
作為方法引數的作用。實際上,Java的泛型還允許使用無限定萬用字元(Unbounded Wildcard Type),即只定義一個?
:
void sample(Pair<?> p) {
}
因為<?>
萬用字元既沒有extends
,也沒有super
,因此:
- 不允許呼叫
set(T)
方法並傳入引用(null
除外); - 不允許呼叫
T get()
方法並獲取T
引用(只能獲取Object
引用)。
換句話說,既不能讀,也不能寫,那隻能做一些null
判斷:
static boolean isNull(Pair<?> p) {
return p.getFirst() == null || p.getLast() == null;
}
大多數情況下,可以引入泛型引數<T>
消除<?>
萬用字元:
static <T> boolean isNull(Pair<T> p) {
return p.getFirst() == null || p.getLast() == null;
}
<?>
萬用字元有一個獨特的特點,就是:Pair<?>
是所有Pair<T>
的超類:
public class Main {
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
}
上述程式碼是可以正常編譯執行的,因為Pair<Integer>
是Pair<?>
的子類,可以安全地向上轉型。
小結
使用類似<? super Integer>
萬用字元作為方法引數時表示:
-
方法內部可以呼叫傳入
Integer
引用的方法,例如:obj.setFirst(Integer n);
; -
方法內部無法呼叫獲取
Integer
引用的方法(Object
除外),例如:Integer n = obj.getFirst();
。
即使用super
萬用字元表示只能寫不能讀。
使用extends
和super
萬用字元要遵循PECS原則。
無限定萬用字元<?>
很少使用,可以用<T>
替換,同時它是所有<T>
型別的超類。