[TOC]
深入理解 Java 泛型
概述
泛型的本質(zhì)是參數(shù)化類型,通常用于輸入?yún)?shù)拦宣、存儲類型不確定的場景。相比于直接使用 Object 的好處是:編譯期強(qiáng)類型檢查、無需進(jìn)行顯式類型轉(zhuǎn)換届腐。
類型擦除
Java 中的泛型是在編譯器這個(gè)層次實(shí)現(xiàn)的,在生成的Java字節(jié)代碼中是不包含泛型中的類型信息的蜂奸。使用泛型的時(shí)候加上的類型參數(shù)犁苏,會(huì)被編譯器在編譯的時(shí)候去掉。這個(gè)過程就稱為類型擦除 type erasure扩所。
public class Test {
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(strList.getClass().getName());
System.out.println(intList.getClass().getName());
}
}
上面這一段代碼围详,運(yùn)行后輸出如下,可知在運(yùn)行時(shí)獲取的類型信息是不帶具體類型的:
java.util.ArrayList
java.util.ArrayList
類型擦除也是 Java 的泛型實(shí)現(xiàn)方式與 C++ 模板機(jī)制實(shí)現(xiàn)方式之間的重要區(qū)別。這就導(dǎo)致:
泛型類并沒有自己獨(dú)有的Class類對象助赞,只有List.class买羞。
運(yùn)行時(shí)無法獲得泛型的真實(shí)類型信息。
比如在 反序列化 Json 串至 List 字符串時(shí)雹食,需要這么做:
public class Test {
public static final ObjectMapper mapper = new ObjectMapper();
public static void main(String[] args) throws IOException {
JavaType javaType = getCollectionType(ArrayList.class, String.class);
List<String> lst = mapper.readValue("[\"hello\",\"world\"]", javaType);
System.out.println(lst);
}
// 獲取泛型的Collection Type
public static JavaType getCollectionType(Class<?> collectionClass, Class<?>... elementClasses) {
return mapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
}
}
Debug 發(fā)現(xiàn) getCollectionType 方法輸出的是 CollectionType 對象畜普,里面存儲了元素類型 _elementType。這就相當(dāng)于把 List 的元素類型 String.class 作為參數(shù)群叶,提供給了 Jackson 去反序列化吃挑。而下面的做法會(huì)編譯失敗:
public class Test {
public static final ObjectMapper mapper = new ObjectMapper();
public static void main(String[] args) throws IOException {
List<String> lst = mapper.readValue("[\"hello\",\"world\"]", List<String>.class); // 編譯錯(cuò)誤
System.out.println(lst);
}
}
泛型不是協(xié)變的
在 Java 語言中盖呼,數(shù)組是協(xié)變的儒鹿,也就是說,如果 Integer 擴(kuò)展了 Number几晤,那么不僅 Integer 是 Number约炎,而且 Integer[] 也是 Number[],在要求 Number[] 的地方完全可以傳遞或者賦予 Integer[]蟹瘾。(更正式地說圾浅,如果 Number是 Integer 的超類型,那么 Number[] 也是 Integer[]的超類型)憾朴。您也許認(rèn)為這一原理同樣適用于泛型類型 —— List< Number> 是 List< Integer> 的超類型狸捕,那么可以在需要 List< Number> 的地方傳遞 List< Integer>。不幸的是众雷,情況并非如此灸拍。為啥呢?這么做將破壞要提供的類型安全泛型砾省。
對于數(shù)組來說鸡岗,下面的代碼會(huì)有運(yùn)行時(shí)錯(cuò)誤:
public class Test {
public static void main(String[] args) {
String[] strArray = new String[3];
Object[] objArray = strArray;
objArray[0] = 1;// 運(yùn)行時(shí)錯(cuò)誤:Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer
}
}
而集合這么寫就會(huì)有編譯錯(cuò)誤:
public class Test {
public static void main(String[] args) {
List<String> strList = new ArrayList<>();
// 編譯 Error:(14, 32) java: 不兼容的類型: java.util.List<java.lang.String>無法轉(zhuǎn)換為java.util.List<java.lang.Object>
List<Object> objList = strList;
}
}
數(shù)組能夠協(xié)變而泛型不能協(xié)變的另一個(gè)后果是,不能實(shí)例化泛型類型的數(shù)組(new List< String>[3] 是不合法的)编兄,除非類型參數(shù)是一個(gè)未綁定的通配符(new List< ?>[3]是合法的)轩性。具體可以運(yùn)行下面的代碼看看:
public class Test {
public static void main(String[] args) {
// 編譯正常
List<?>[] lsa2 = new List<?>[10];
// 編譯 Error:(14, 30) java: 創(chuàng)建泛型數(shù)組
List<String>[] lsa = new List<String>[10];
}
}
構(gòu)造延遲
因?yàn)檫\(yùn)行時(shí)不能區(qū)分 List< String> 和 List< Integer>(運(yùn)行時(shí)都是 List),用泛型類型參數(shù)標(biāo)識類型的變量的構(gòu)造就成了問題狠鸳。運(yùn)行時(shí)缺乏類型信息揣苏,這給泛型容器類和希望創(chuàng)建保護(hù)性副本的泛型類提出了難題。比如:
不能使用類型參數(shù)訪問構(gòu)造函數(shù)
您不能使用類型參數(shù)訪問構(gòu)造函數(shù)件舵,因?yàn)樵诰幾g的時(shí)候還不知道要構(gòu)造什么類卸察,因此也就不知道使用什么構(gòu)造函數(shù)。使用泛型不能表達(dá)“T必須擁有一個(gè)拷貝構(gòu)造函數(shù)(copy constructor)”(甚至一個(gè)無參數(shù)的構(gòu)造函數(shù))這類約束铅祸,因此不能使用泛型類型參數(shù)所表示的類的構(gòu)造函數(shù)坑质。
public class Test {
public <T> void doSomething(T param) {
T copy = new T(param); // 編譯錯(cuò)誤:Error:(13, 22) java: 意外的類型,需要: 類,找到: 類型參數(shù)T
}
}
不能使用 clone 方法
為什么呢洪乍?因?yàn)?clone() 在 Object 中是 protected 保護(hù)訪問的眯杏,調(diào)用 clone() 必須通過將 clone() 改寫為 public 公共訪問的類方法來完成。但是 T 的 clone() 是否為 public 是無法確定的壳澳,因此調(diào)用其 clone 也是非法的岂贩。
public class Test {
public <T> void doSomething(T param) {
T copy = (T) param.clone(); // 編譯 Error:(13, 27) java: clone()在java.lang.Object中訪問protected
}
}
不能創(chuàng)建泛型數(shù)組
不能實(shí)例化用類型參數(shù)表示的類型數(shù)組。編譯器不知道 T 到底表示什么類型巷波,因此不能實(shí)例化 T 數(shù)組萎津。
public class Test {
public <T> void doS() {
T[] t = new T[5];
}
}
那么 ArrayList 是如何存儲數(shù)據(jù)的呢?請看下面的源代碼抹镊,是用 Object 數(shù)組存儲的锉屈,所以在獲取元素時(shí)要做顯示類型轉(zhuǎn)換(在 elementData 方法中):
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
transient Object[] elementData; // Object 數(shù)組存儲數(shù)據(jù)
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
@SuppressWarnings("unchecked")
E elementData(int index) {
return (E) elementData[index];
}
}
通配符 extends 和 super
在泛型不是協(xié)變中提到,在使用 List< Number> 的地方不能傳遞 List< Integer>垮耳,那么有沒有辦法能讓他兩兼容使用呢颈渊?答案是:有,可以使用通配符终佛。
泛型中 ? 可以用來做通配符俊嗽,單純 ? 匹配任意類型。< ? extends T > 表示類型的上界是 T铃彰,參數(shù)化類型可能是 T 或 T 的子類:
public class Test {
static class Food {}
static class Fruit extends Food {}
static class Apple extends Fruit {}
public static void main(String[] args) throws IOException {
List<? extends Fruit> fruits = new ArrayList<>();
fruits.add(new Food()); // compile error
fruits.add(new Fruit()); // compile error
fruits.add(new Apple()); // compile error
fruits = new ArrayList<Fruit>(); // compile success
fruits = new ArrayList<Apple>(); // compile success
fruits = new ArrayList<Food>(); // compile error
fruits = new ArrayList<? extends Fruit>(); // compile error: 通配符類型無法實(shí)例化
Fruit object = fruits.get(0); // compile success
}
}
從上面代碼中可以看出來绍豁,賦值是參數(shù)化類型為 Fruit 和其子類的集合都可以成功,通配符類型無法實(shí)例化牙捉。為啥上面代碼中的 add 全部編譯失敗了呢竹揍?因?yàn)?fruits 集合并不知道實(shí)際類型是 Fruit、Apple 還是 Food邪铲,所以無法對其賦值芬位。
除了 extends 還有一個(gè)通配符 super,< ? super T > 表示類型的下界是 T霜浴,參數(shù)化類型可以是 T 或 T 的超類:
public class Test {
static class Food {}
static class Fruit extends Food {}
static class Apple extends Fruit {}
public static void main(String[] args) throws IOException {
List<? super Fruit> fruits = new ArrayList<>();
fruits.add(new Food()); // compile error
fruits.add(new Fruit()); // compile success
fruits.add(new Apple()); // compile success
fruits = new ArrayList<Fruit>(); // compile success
fruits = new ArrayList<Apple>(); // compile error
fruits = new ArrayList<Food>(); // compile success
fruits = new ArrayList<? super Fruit>(); // compile error: 通配符類型無法實(shí)例化
Fruit object = fruits.get(0); // compile error
}
}
看上面代碼可知晶衷,super 通配符類型同樣不能實(shí)例化蓝纲,F(xiàn)ruit 和其超類的集合均可賦值阴孟。這里 add 時(shí) Fruit 及其子類均可成功,為啥呢税迷?因?yàn)橐阎?fruits 的參數(shù)化類型必定是 Fruit 或其超類 T永丝,那么 Fruit 及其子類肯定可以賦值給 T。
歸根到底箭养,還是“子類對象可以賦值給超類引用慕嚷,而反過來不行”這一規(guī)則導(dǎo)致 extends 和 super 通配符在 add 操作上表現(xiàn)如此的不同。同樣地,也導(dǎo)致 super 限定的 fruits 中 get 到的元素不能賦值給 Fruit 引用喝检,而 extends 則可以嗅辣。
總結(jié)一下就是:
- extends 可用于的返回類型限定,不能用于參數(shù)類型限定挠说。
- super 可用于參數(shù)類型限定澡谭,不能用于返回類型限定。
- 帶有 super 超類型限定的通配符可以向泛型對易用寫入损俭,帶有 extends 子類型限定的通配符可以向泛型對象讀取蛙奖。
運(yùn)行時(shí)泛型參數(shù)類型獲取
雖然 Java 的泛型在編譯期間有類型擦除,但是如果真的需要在運(yùn)行時(shí)知道泛型參數(shù)的類型杆兵,應(yīng)該如何做呢雁仲?
額外保存參數(shù)類型
在上面“類型擦除”中提到了 Jackson 反序列化泛型類型,將參數(shù)類型信息顯式保存下來琐脏。
public class TestJackson {
public static final ObjectMapper mapper = new ObjectMapper();
public static void main(String[] args) throws IOException {
JavaType javaType = getCollectionType(ArrayList.class, String.class);
List<String> lst = mapper.readValue("[\"hello\",\"world\"]", javaType);
System.out.println(lst);
}
// 獲取泛型的Collection Type
public static JavaType getCollectionType(Class<?> collectionClass, Class<?>... elementClasses) {
return mapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
}
}
經(jīng)過 Debug 發(fā)現(xiàn)攒砖,getCollectionType 返回的對象實(shí)際類型是 CollectionType:
CollectionType 和 JavaType 之間的繼承關(guān)系,可以看下面的代碼:
public final class CollectionType extends CollectionLikeType {
}
public class CollectionLikeType extends TypeBase {
protected final JavaType _elementType;
}
public abstract class TypeBase extends JavaType implements JsonSerializable {
protected final JavaType _superClass;
protected final JavaType[] _superInterfaces;
protected final TypeBindings _bindings;
}
注解處理器
我們可以使用注解處理器日裙,在編譯期間獲取泛型真實(shí)類型祭衩,并保存到類文件中,詳見 Java 注解:注解處理器獲取泛型真實(shí)類型阅签。
這個(gè)方法的本質(zhì)也是“額外保存參數(shù)類型”掐暮,只不過方法不同罷了。
signature 屬性
Java泛型的擦除并不是對所有使用泛型的地方都會(huì)擦除的政钟,部分地方會(huì)保留泛型信息路克。比如 java.lang.reflect.Field 類中有一個(gè) signature 屬性保存了泛型的參數(shù)類型信息,通過 Field 的 getGenericType 方法即可得到养交。當(dāng)然精算,這種方法僅限于類中的 屬性,對于方法中的局部變量無能為力碎连。
public final class Field extends AccessibleObject implements Member {
private transient String signature;
private String getGenericSignature() {return signature;}
public Type getGenericType() {
if (getGenericSignature() != null)
return getGenericInfo().getGenericType();
else
return getType();
}
}
運(yùn)行時(shí)能夠獲取泛型參數(shù)類型灰羽,根源在于字節(jié)碼中還是包含了這些信息的,對于下面這樣一個(gè)類:
public class Pojo {
private String str;
private List<Integer> intList;
private int i;
}
使用 javac Pojo.java 命令編譯之后鱼辙,使用 javap -verbose Pojo.class 命令查看其字節(jié)碼信息廉嚼,可以看到常量池中,緊跟 intList 屬性存儲的就是其 Signature倒戏。
Constant pool:
#1 = Methodref #3.#21 // java/lang/Object."<init>":()V
#2 = Class #22 // Pojo
#3 = Class #23 // java/lang/Object
#4 = Utf8 str
#5 = Utf8 Ljava/lang/String;
#6 = Utf8 intList
#7 = Utf8 Ljava/util/List;
#8 = Utf8 Signature
#9 = Utf8 Ljava/util/List<Ljava/lang/Integer;>;
#10 = Utf8 i
#11 = Utf8 I
Field 可以獲取到泛型參數(shù)信息怠噪,類似地 Class 也是可以的。下面直接上代碼看如何獲取吧杜跷。
示例: Field
public class Pojo {
private List<Integer> intList;
}
public class Test {
public static void main(String[] args) throws NoSuchFieldException {
Field intListField = Pojo.class.getDeclaredField("intList");
Type genericType = intListField.getGenericType();
Class<?> parameterType = (Class<?>) ((ParameterizedType) genericType).getActualTypeArguments()[0];
System.out.println(parameterType);
}
}
執(zhí)行以后傍念,輸出:
class java.lang.Integer
示例:Class
public abstract class AbsClass<T> {
protected final Type _type;
public AbsClass() {
Type superClass = getClass().getGenericSuperclass();
_type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
}
public Type getParameterizeType() {
return _type;
}
}
public class ParaClass extends AbsClass<Long> {
}
public class Test {
public static void main(String[] args) throws NoSuchFieldException {
ParaClass paraClass = new ParaClass();
System.out.println(paraClass.getParameterizeType());
}
}
執(zhí)行以后矫夷,輸出:
class java.lang.Long
這里 ParaClass 繼承的是 AbsClass< Long>,而非 AbsClass< T>憋槐。于是双藕,對 ParaClass.class 調(diào)用 getGenericSuperclass(),就可以進(jìn)一步獲取到 T 所綁定的 Long 類型阳仔。
有木有發(fā)現(xiàn)蔓彩,這兩個(gè)示例的共同點(diǎn)是,都用到了 ParameterizedType.getActualTypeArguments()[0] 這一句驳概,因?yàn)榉盒偷膮?shù)類型也就是存在了這里赤嚼。