??面試被問到:泛型是怎么被擦除的?愣是說不出個所以然來...
其實了解泛型的擦拭法才能更好地使用和編寫泛型。基礎(chǔ)永遠不能忘~!
- 1.使用泛型
- 2.編寫泛型
- 3.擦拭法
- 4.extends通配符
- 5.super通配符
- 6.泛型和反射
1.使用泛型
泛型是一種“代碼模板”,可以用一套代碼套用各種類型。例如ArrayList<T>,然后在代碼中為用到的類創(chuàng)建對應(yīng)的ArrayList<類型>:
ArrayList<String> strList = new ArrayList<可省略>();
由編譯器針對類型作檢查:
strList.add("hello"); // OK
String s = strList.get(0); // OK
strList.add(new Integer(123)); // compile error!
Integer n = strList.get(0); // compile error!
向上轉(zhuǎn)型
在Java標(biāo)準庫中的ArrayList<T>實現(xiàn)了List<T>接口,它可以向上轉(zhuǎn)型為List<T>:
public class ArrayList<T> implements List<T> {
...
}
List<String> list = new ArrayList<String>();
要特別注意:不能把ArrayList<Integer>向上轉(zhuǎn)型為ArrayList<Number>或List<Number>。
???這是為什么呢?假設(shè)ArrayList<Integer>可以向上轉(zhuǎn)型為ArrayList<Number>,觀察一下代碼:
// 創(chuàng)建ArrayList<Integer>類型:
ArrayList<Integer> integerList = new ArrayList<Integer>();
// 添加一個Integer:
integerList.add(new Integer(123));
// “向上轉(zhuǎn)型”為ArrayList<Number>:
ArrayList<Number> numberList = integerList;
// 添加一個Float,因為Float也是Number:
numberList.add(new Float(12.34));
// 從ArrayList<Integer>獲取索引為1的元素(即添加的Float):
Integer n = integerList.get(1); // ClassCastException!
實際上,編譯器為了避免這種錯誤,根本就不允許??。
??泛型的好處是:使用時不必對類型進行強制轉(zhuǎn)換,它通過編譯器對類型進行檢查。
2.編寫泛型
編寫泛型類比普通類要復(fù)雜。通常來說,泛型類一般用在集合類中,例如ArrayList<T>,我們很少需要編寫泛型類。
可我們一定要編寫的話,首先,按照某種類型,例如:String,來編寫類:
public class Pair {
private String first;
private String last;
public Pair(String first, String last) {
this.first = first;
this.last = last;
}
public String getFirst() {
return first;
}
public String getLast() {
return last;
}
}
然后,把特定類型String替換為T,并申明<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;
}
}
??????編寫泛型類時,要特別注意,泛型類型<T>不能用于靜態(tài)方法。
對于靜態(tài)方法,我們可以單獨改寫為“泛型”方法,只需要使用另一個類型即可。對于上面的create()靜態(tài)方法,我們應(yīng)該把它改為另一種泛型類型,例如,<K>:
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() { ... }
public T getLast() { ... }
// 靜態(tài)泛型方法應(yīng)該使用其他類型區(qū)分:
public static <K> Pair<K> create(K first, K last) {
return new Pair<K>(first, last);
}
}
這樣才能清楚地將靜態(tài)方法的泛型類型和實例類型的泛型類型區(qū)分開。
多個泛型類型
泛型還可以定義多種類型。例如,我們希望Pair不總是存儲兩個類型一樣的對象,就可以使用類型<T, K>:
public class Pair<T, K> {
private T first;
private K last;
public Pair(T first, K last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public K getLast() { ... }
}
// 使用的時候,需要指出兩種類型:
Pair<String, Integer> p = new Pair<>("test", 123);
Java標(biāo)準庫的Map<K, V>就是使用兩種泛型類型的例子。它對Key使用一種類型,對Value使用另一種類型。
3.擦拭法
泛型是一種類似”模板代碼“的技術(shù),不同語言的泛型實現(xiàn)方式不一定相同。
- Java語言的泛型實現(xiàn)方式是擦拭法(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;
}
}
// 使用:
Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();
而虛擬機根本不知道泛型。這才是虛擬機執(zhí)行的代碼:
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;
}
}
// 使用:
Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();
因此,Java使用擦拭法實現(xiàn)泛型,導(dǎo)致了:
- 編譯器把類型
<T>視為Object - 編譯器根據(jù)
<T>實現(xiàn)安全的強制轉(zhuǎn)型
??所以,重點來了??????Java的泛型是由編譯器在編譯時實行的,編譯器內(nèi)部永遠把所有類型T視為Object處理,但是,在需要轉(zhuǎn)型的時候,編譯器會根據(jù)T的類型自動為我們實行安全地強制轉(zhuǎn)型。
??了解了Java泛型的實現(xiàn)方式——擦拭法,我們就知道了Java泛型的局限:
- 局限一:
<T>不能是基本類型,例如int,因為實際類型是Object,Object類型無法持有基本類型 - 局限二:無法取得帶泛型的
Class。觀察以下代碼:
public class Main {
public static void main(String[] args) {
Pair<String> p1 = new Pair<>("Hello", "world");
Pair<Integer> p2 = new Pair<>(123, 456);
Class c1 = p1.getClass();
Class c2 = p2.getClass();
System.out.println(c1==c2); // true
System.out.println(c1==Pair.class); // true
}
}
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> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>.class) {
}
無論T的類型是什么,getClass()返回同一個Class實例,因為編譯后它們?nèi)慷际?code>Pair<Object>,因此也無法判斷帶泛型的Class的類型:。
- 局限四:不能實例化T類型:
public class Pair<T> {
private T first;
private T last;
public Pair() {
// Compile error:
first = new T();
last = new T();
}
}
這樣一來,創(chuàng)建new Pair<String>()和創(chuàng)建new Pair<Integer>()就全部成了Object,顯然編譯器要阻止這種類型不對的代碼。
要實例化T類型,我們必須借助額外的Class<T>參數(shù):
public class Pair<T> {
private T first;
private T last;
public Pair(Class<T> clazz) {
first = clazz.newInstance();
last = clazz.newInstance();
}
}
上述代碼借助Class<T>參數(shù)并通過反射來實例化T類型,使用的時候,也必須傳入Class<T>。例如:
Pair<String> pair = new Pair<>(String.class);
因為傳入了Class<String>的實例,所以我們借助String.class就可以實例化String類型。
泛型繼承
在繼承了泛型類型的情況下,子類可以獲取父類的泛型類型。例如:IntPair可以獲取到父類的泛型類型Integer。獲取父類的泛型類型代碼比較復(fù)雜:
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
public class Main {
public static void main(String[] args) {
Class<IntPair> clazz = IntPair.class;
Type t = clazz.getGenericSuperclass();
if (t instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) t;
Type[] types = pt.getActualTypeArguments(); // 可能有多個泛型類型
Type firstType = types[0]; // 取第一個泛型類型
Class<?> typeClass = (Class<?>) firstType;
System.out.println(typeClass); // Integer
}
}
}
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來標(biāo)識類型已經(jīng)不夠了。??實際上,Java的類型系統(tǒng)結(jié)構(gòu)如下:

4.extends通配符
我們前面已經(jīng)講到了泛型的繼承關(guān)系:·Pair<Integer>不是Pair<Number>的子類。
??可是我們想傳遞Pair<Integer>這怎么辦呢?
??這時候我們可以extends通配符來限定T的類型:
public class Pair<T extends Number> { ... }
現(xiàn)在,我們可以這樣定義了,因為Number、Integer和Double都符合<T extends Number>:
Pair<Number> p1 = null;
Pair<Integer> p2 = new Pair<>(1, 2);
Pair<Double> p3 = null;
5.super通配符
需求:和extends通配符相反,這次,我們希望接受Pair<Integer>類型,以及Pair<Number>、Pair<Object>,因為Number和Object是Integer的父類,我們這樣定義:
public class Pair<T super Integer> { ... }
現(xiàn)在,我們可以定義了,因為Number、Integer和Object都是父類<T super Integer>:
Pair<Number> p1 = null;
Pair<Integer> p2 = new Pair<>(1, 2);
Pair<Object> p3 = null;
好啦,以上使我們必須要掌握的語法。
華麗的分割線
以上是用在定義泛型類型時通配符的使用,其實泛型還可被定義在方法參數(shù)上,以下知識點容易頭疼......
下面代碼說明了get和set時兩種情況:
public class Main {
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
int n = getNum(p);
System.out.println(n);
Pair<Number> p1 = new Pair<>(12.3, 4.56);
Pair<Integer> p2 = new Pair<>(123, 456);
setSame(p1, 100);
setSame(p2, 200);
System.out.println(p1.getFirst() + ", " + p1.getLast());
System.out.println(p2.getFirst() + ", " + p2.getLast());
}
static int getNum(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
static void setSame(Pair<? super Integer> p, Integer n) {
p.setFirst(n);
p.setLast(n);
}
}
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;
}
}
作為方法參數(shù),<? extends T>類型和<? super T>類型的區(qū)別在于:
-
<? extends T>允許調(diào)用讀方法T get()獲取T的引用,但不允許調(diào)用寫方法set(T)傳入T的引用(傳入null除外); -
<? super T>允許調(diào)用寫方法set(T)傳入T的引用,但不允許調(diào)用讀方法T get()獲取T的引用(獲取Object除外)。
一個是允許讀不允許寫,另一個是允許寫不允許讀。
先記住上面的結(jié)論,我們來看Java標(biāo)準庫的Collections類定義的copy()方法:
public class Collections {
// 把src的每個元素復(fù)制到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中。
它的第一個參數(shù)是List<? super T>,表示目標(biāo)List,第二個參數(shù)List<? extends T>,表示要復(fù)制的List。
我們可以簡單地用for循環(huán)實現(xiàn)復(fù)制。在for循環(huán)中,我們可以看到:
- 對于類型
<? extends T>的變量src,我們可以安全地獲取類型T的引用, - 而對于類型
<? super T>的變量dest,我們可以安全地傳入T的引用。*
這個copy()方法的定義就完美地展示了extends和super的意圖:
-
copy()方法內(nèi)部不會讀取dest,因為不能調(diào)用dest.get()來獲取T的引用; -
copy()方法內(nèi)部也不會修改src,因為不能調(diào)用src.add(T)。
這是由編譯器檢查來實現(xiàn)的。如果在方法代碼中意外修改了src,或者意外讀取了dest,就會導(dǎo)致一個編譯錯誤:
public class Collections {
// 把src的每個元素復(fù)制到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通配符,并由編譯器強制檢查來實現(xiàn)的。
無限定通配符
我們已經(jīng)討論了<? extends T>和<? super T>作為方法參數(shù)的作用。實際上,Java的泛型還允許使用無限定通配符(Unbounded Wildcard Type),即只定義一個?:
void sample(Pair<?> p) {
}
因為<?>通配符既沒有extends,也沒有super,因此:
- 不允許調(diào)用
set(T)方法并傳入引用(null除外); - 不允許調(diào)用
T get()方法并獲取T引用(只能獲取Object引用)。
換句話說,既不能讀,也不能寫,那只能做一些null判斷:
static boolean isNull(Pair<?> p) {
return p.getFirst() == null || p.getLast() == null;
}
大多數(shù)情況下,可以引入泛型參數(shù)<T>消除<?>通配符:
static <T> boolean isNull(Pair<T> p) {
return p.getFirst() == null || p.getLast() == null;
}
<?>通配符有一個獨特的特點,就是:Pair<?>是所有Pair<T>的超類:
public class Main {
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
Pair<?> p2 = p; // 安全地向上轉(zhuǎn)型
System.out.println(p2.getFirst() + ", " + p2.getLast());
}
}
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<?>的子類,可以安全地向上轉(zhuǎn)型。
6.泛型和反射
Java的部分反射API也是泛型。例如:Class<T>就是泛型:
// compile warning:
Class clazz = String.class;
String str = (String) clazz.newInstance();
// no warning:
Class<String> clazz = String.class;
String str = clazz.newInstance();
調(diào)用Class的getSuperclass()方法返回的Class類型是Class<? super T>:
Class<? super String> sup = String.class.getSuperclass();
構(gòu)造方法Constructor<T>也是泛型:
Class<Integer> clazz = Integer.class;
Constructor<Integer> cons = clazz.getConstructor(int.class);
Integer i = cons.newInstance(123);
??使用<T>泛型時,要帶著最終被擦除的思想去設(shè)計代碼,要知道最終是會被改寫成Object的,這樣才能使用好和編寫好泛型!
泛型的使用就到這里,其他語法使用:Java基本功系列之?注解Annotation
(End)