?
和 T
是什么?
?
是通配符(wildcard,記住這個單詞,這樣在報錯時就知道說的是 ? 通配符),T
是類型變量。
- 根據字面意思,
<? extends T>
表示 任何繼承自類型T
的類型,<? super T>
表示 任何是類型T
的超類的類型。
? extend/super Xx
不能用于聲明處。
下面是用于表達式的正確例子:Plate<? super Fruit> p = new Plate<Fruit>(new Fruit()); Plate<? extend Fruit> p1;
下面是用于聲明處的錯誤例子:
public class Fruit {} public class Plate<? extends Fruit> {} // 報錯 Unexpected wildcard
-
T
則與問號不同,它只能用于聲明處。<T extends Fruit>
表示聲明一個類型變量T
,它是Fruit
的一個具體子類。不能寫<T super Fruit>
,因為這樣是沒有任何意義的。原因是所有泛型在編譯時都會被擦除,T
所代表的是一個Fruit
的超類,但是具體是哪個類卻是在運行時被決定的,編譯器為了類型安全,只能做最大限度的包容,因此所有的T
類型都會在編譯器變為Object
。所以,寫<T super Fruit>
等同于寫<Object>
,因此不支持<T super Xx>
。
上界和下界
下面代碼就是上界通配符(Upper Bounds Wildcards)
Plate<? extends Fruit> plate;
它的表意是,一個能放 Fruit
及其子類的盤子。
下面代碼就是下界通配符(Lower Bounds Wildcards)
Plate<? super Fruit> plate
它的表意是,一個能放 Fruit
及其父類的盤子。
PECS 原則
以下原則,我們都先假設有這樣的類:
class Plate<T> {
private T item;
public Plate(T t) {
item = t;
}
public void set(T t) {
item = t;
}
public T get() {
return item;
}
}
1. <? extends T> 不能往里存,只能往外取(被稱作協變)
- 往里存的意思就是,不能調用
<? extends T>
泛型類的以T
為形參的方法。 - 往外取的意思就是,可以調用
<? extends T>
泛型類的以T
為返回值的方法。
Plate<? extends Fruit> p = new Plate<Apple>(new Apple()); // 實例 p 是協變的
// 不能被存入任何元素
p.set(new Fruit()); // exception
p.set(new Apple()); // exception
// 取出來的東西只能放在 Fruit 或它的基類里
Fruit fruit = p.get();
Object object = p.get();
Apple apple = p.get(); // exception
注意以上代碼,只有在構造函數中可以對
T
類型的item
進行賦值,通過set
方法進行賦值是不行的。原因是編譯器通過<? extends Fruit>
只知道p
接受的是Fruit
及其子類,但是具體是哪個不能確定。但是從寫法上,我們在構造時已經顯式地指明了p
的泛型類型是Apple
,為什么編譯器還不知道呢?這個涉及到類型擦除。這是因為 JVM 在設計初期就沒有考慮過泛型,因此對于 JVM 編譯成的字節碼來說,也沒有泛型的概念,JVM 會使用一個占位符 CAP#1 來表示p
接受一個Fruit
或子類,這里就通過 CAP#1 把類型擦除了。所以無論想往p
插入任何類型都不可以(因為你不能賦值一個 CAP#1 類型)。但是你可以從p
中往外取 CAP#1,因為 CAP#1 代表的是Fruit
及其子類,因此往外取時,類型為Fruit
及其超類就總是安全的。但是,將
<? extends Fruit>
替換為一個具體的類型,set
方法就是生效的。這是因為編譯器已經知道p
只會接受一個確定類型的水果Apple
。
Plate<Apple> applePlate = new Plate<>(new Apple()); // 實例 applePlate 是不變的
applePlate.set(new Apple());
Apple apple = applePlate.get();
applePlate.set(new GreenApple());
- 但是下面的寫法是不被接受的。因為泛型
T
是一個類型變量,它不能被用于表達式中,只能被用于聲明處。
Plate<T extends Fruit> p = new Plate<>(new Apple());
- 如果寫
Plate<?>
,則表示Plate
中放的是任意類型,因此什么也存不進去,可以往外取Object
。因為<?>
隱式地表示<? extends Object>
。
Plate<?> anyPlate = new Plate<>(new Apple());
Object o = anyPlate.get();
其實存不進去的根本原因是因為,Java 類沒有一個共同的子類,但是卻有一個共同的父類
Object
。所以永遠可以向上轉型取數據,卻不能向下轉型存數據。
2. 下界 <? super Fruit> 不影響往里存,但是往外取只能放在 Object
(被稱作逆變)
使用下界 <? super Fruit>
的意思是,Plate
中存放的是任意 Fruit
的基類,但是不確定是哪一個。因此往里放 Fruit
以及其子類一定是可以的(因為這些類一定是 <? super Fruit>
的子類)。但是往外取時就只能是 Object
,因為編譯器不知道你存的是 Fruit
還是 Meat
。Fruit
和 Meat
就只有一個共同父類,那就是 Object
。因此往外取 Object
一定是對的。
3. PECS(Producer Extends Consumer Super)
3.1 Producer Extends 你寫的類是主要作為生產者向外提供數據,那么就用 extends
3.2 Consumer Super 你寫的類是主要作為消費者,需要吃進數據,那么就用 super
4. 看一個復雜點的例子
class X< T extends List<? extends Number> > {
public void someMethod(T t) {
t.add(new Long(0L)); // error
Number n = t.remove(0);
}
}
class Test {
public static void main(String[] args) {
X<ArrayList< Long >> x1 = new X<ArrayList<Long>>();
X<ArrayList< String >> x2 = new X<ArrayList<String>>(); // error
}
}
類 X
聲明了一個類型參數 T
。T
是一個協變類型 List<? extends Number>
的子類。因此類 X
的方法 someMethod(T t)
可以接受一個類型為 T
的參數 t
,但是不能往 t
里放任何數據。因為 <? extends Number>
限制了只能往外取。
這里的協變就是使用處協變。泛型聲明
T extends List<? extends Number>
并沒有直接限制X
的類方法聲明的地方對T
的使用,即X
的類方法依然可以接受T
的入參。但是在 Kotlin 中,我們就可以通過在T
前面加上關鍵字out
來使T
在聲明處就產生協變。
5. 自限定類型
class SelfBounded<T extends SelfBounded<T>>
這個寫法剛看起來寧人十分疑惑。聲明了一個類型 T
,它是 SelfBounded<T>
的子類型,這似乎是一個無限循環。其實自限定類型是為子類提供了一個模板,泛型 T
在子類中只能表示子類這個具體類,而不能是其他類型。
interface SelfBoundSetter<T extends SelfBoundSetter<T>> {
void set(T args);
}
interface Setter extends SelfBoundSetter<Setter> {}
public class SelfBoundAndCovariantArguments {
void testA(Setter s1, Setter s2, SelfBoundSetter sbs) {
s1.set(s2);
s1.set(sbs); // 編譯錯誤
}
}
5.1 自限定類型的具體例子
如果你去看 Enum
的源碼,就會發現,Enum
的聲明是自限定的類型。
public abstract class Enum<E extends Enum<E>> {
...
}
一個具體的枚舉類型 Color
在編譯時就被翻譯成了 Color extends Enum<Color>
。如我們前面所說,自限定類型主要目的是為子類提供一種模板,我們來看看枚舉類 Enum
是怎么用這個模板的:
public abstract class Enum< E extends Enum<E>> implements Comparable< E >, Serializable {
private final String name;
public final String name() { ... }
private final int ordinal;
public final int ordinal() { ... }
protected Enum(String name, int ordinal) { ... }
public String toString() { ... }
public final boolean equals(Object other) { ... }
public final int hashCode() { ... }
protected final Object clone() throws CloneNotSupportedException { ... }
public final int compareTo( E o) { ... }
public final Class< E > getDeclaringClass() { ... }
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { ... }
}
如果我們聲明一個具體的媒體類型 Color
:
enum Color {RED, BLUE, GREEN}
編譯器會將它翻譯成:
public final class Color extends Enum<Color> {
public static final Color[] values() { return (Color[])$VALUES.clone(); }
public static Color valueOf(String name) { ... }
private Color(String s, int i) { super(s, i); }
public static final Color RED;
public static final Color BLUE;
public static final Color GREEN;
private static final Color $VALUES[];
static {
RED = new Color("RED", 0);
BLUE = new Color("BLUE", 1);
GREEN = new Color("GREEN", 2);
$VALUES = (new Color[] { RED, BLUE, GREEN });
}
}
其中 Color
的 Color.compareTo
方法應該使用一個 Color
類型作為參數,自限定類型剛好就能滿足這樣的模板。
5.2 自限定類型的缺點
從名字上來看,自限定這個名字會讓我們誤以為像上面的 Color
類一樣,其中的泛型方法只能接受子類自己,比如 Color.compareTo
只能接受 Color
類型的參數。其實不然,其實自限定類型還可以接受它的兄弟類。
public interface SelfBound<E extends SelfBound<E>> {}
public class SelfBound1 implements SelfBound<SelfBound1> {}
public class SelfBound2 implements SelfBound<SelfBound1> {} // 注意這里接受的泛型實參為 SelfBound1
SelfBound2
接受了它的兄弟類型 SelfBound1
,而不是像 SelfBound1
一樣接受的自己。如果以為 SelfBound2
的聲明只能接受它自己 SelfBound2
,那就完全錯誤了。因此自限定這個叫法有點名不符其實。
6. 捕獲轉換
<?>
被稱作無界通配符,它有一個特殊的應用場景叫做捕獲轉換。
public class CaptureConversion {
static <T> void f1(Holder<T> holder) {
T t = holder.get();
System.out.println(t.getClass().getSimpleName());
}
static void f2(Holder<?> holder) {
// 做一些與 Holder 中與泛型類型無關的事,然后再調用 f1(holder),
// 將 Holder 中的類型給捕獲
f1(holder);
}
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Holder raw = new Holder<Integer>(1);
f2(raw);
Holder rawBasic = new Holder();
rawBasic.set(new Object());
f2(rawBasic);
Holder<?> wildcarded = new Holder<Double>(1.0);
f2(wildcarded);
}
static class Holder<T> {
private T t;
public Holder(){}
public Holder(T t){}
public void set(T t){
this.t = t;
}
public T get() {
return t;
}
}
}
/* 程序輸出
Integer
Object
Double
*/
類型捕獲的地方當然可以寫已聲明的類型 T
。但是這樣會降低程序的可讀性,因為在 f2()
中根本用不到泛型 T
相關的知識。而且,<?>
使 f2()
產生了協變,讓方法 f2()
中只能從 holder
中取數據,而不能寫數據。但是將 holder
傳給方法 f1()
后,由于方法 f1()
有聲明了不變的泛型 <T>
,因此 holder
又被類型投影為了不變的類型。