你對Java泛型的理解夠深入嗎?
泛型
泛型提供了一種將集合型別傳達給編譯器的方法,一旦編譯器知道了集合元素的型別,編譯器就可以對其型別進行檢查,做型別約束。
在沒有泛型之前:
/** * 迭代 Collection ,注意 Collection 裡面只能是 String 型別 */ public static void forEachStringCollection(Collection collection) { Iterator iterator = collection.iterator(); while (iterator.hasNext()) { String next = (String) iterator.next(); System.out.println("next string : " + next); } }
這是使用泛型之後的程式:
public static void forEachCollection(Collection<String> collection) {
Iterator<String> iterator = collection.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
System.out.println("next string : " + next);
}
}
在沒有泛型之前,我們只能通過更直觀的方法命名和 doc 註釋來告知方法的呼叫者,forEachStringCollection
String
的集合。然而這只是一種“約定”,如果使用方傳入了一個元素不為String
型別的集合,在編譯期間程式碼並不會報錯,只有在執行時,會丟擲ClassCastException
異常,這對呼叫方來說並不友好。
通過泛型,可以將方法的 doc 註釋轉移到了方法簽名上:forEachCollection(Collection<String> collection)
,方法呼叫者一看方法簽名便知道此處需要一個Collection<String>
,編譯器也可以在編譯時檢查是否違反型別約束。需要說明的是,編譯器的檢查也是非常容易繞過的,如何繞過呢?請看下文哦~
畫外音:程式碼就是最好的註釋。
泛型和型別轉化
思考,以下程式碼是否合法:
List<String> strList = new ArrayList<>();
List<Object> objList = new ArrayList<>();
objList.add("公眾號:Coder小黑"); // 程式碼1
objList = strList; // 程式碼2
廢話不多說,直接上答案。
程式碼1
很明顯是合法的。Object
型別是String
型別的父類。
那麼程式碼2
為什麼不合法呢?
在 Java 中,物件型別的賦值其實是引用地址的賦值,也就是說,假設程式碼2
賦值成功,objList
和strList
變數引用的是同一個地址。那會有什麼問題呢?
如果此時,往objList
中添加了一個非String
型別的元素,也就相當於往strList
中添加了一個非String
型別的元素。很明顯,此處就破壞了List<String> strList
。所以,Java 編譯器會認為程式碼2
是非法的,這是一種安全的做法。
畫外音:可能和大多數人的直覺不太一樣,那是我們考慮問題還不夠全面,此處的原因比結果更重要哦
泛型萬用字元
我們已經知道,上文的程式碼2
是不合法的。那麼,接下來思考這樣兩個方法:
public static void printCollection1(Collection c) {}
public static void printCollection2(Collection<Object> c) {}
這兩個方法有什麼區別呢?
printCollection1
方法支援任意元素型別的Collection
,而printCollection2
方法只能接收Object
型別的Collection
。雖然String
是Object
的子類,但是Collection<String>
並不是Collection<Object>
的子類,和程式碼2
有異曲同工之妙。
再看一下下面這個方法:
public static void printCollection3(Collection<?> c) {}
printCollection3
和上面的兩個方法又有什麼區別呢?怎麼理解printCollection3
方法上的?
呢?
?
表示任意型別,表明printCollection3
方法接收任意型別的集合。
好,那麼問題又來了,請看如下程式碼:
List<?> c = Lists.newArrayList(new Object());
Object o = c.get(0);
c.add("12"); // 編譯錯誤
為什麼會編譯報錯呢?
我們可以將任意型別的集合賦值給List<?> c
變數。但是,add
方法的引數型別是?
,它表示未知型別,所以呼叫add
方法時會程式設計錯誤,這是一種安全的做法。
而get
方法返回集合中的元素,雖然集合中的元素型別未知,但是無論是什麼型別,其均為Object
型別,所以使用Object
型別來接收是安全的。
有界萬用字元
public static class Person extends Object {}
public static class Teacher extends Person {}
// 只知道這個泛型的型別是Person的子類,具體是哪一個不知道
public static void method1(List<? extends Person> c) {}
// 只知道這個泛型的型別是Teacher的父類,具體是哪一個不知道
public static void method2(List<? super Teacher> c) {}
思考如下程式碼執行結果:
public static void test3() {
List<Teacher> teachers = Lists.newArrayList(new Teacher(), new Teacher());
// method1 處理的是 Person 的 子類,Teacher 是 Person 的子類
method1(teachers);
}
// 只知道這個泛型的型別是Person的子類,具體是哪一個不知道
public static void method1(List<? extends Person> c) {
// Person 的子類,轉Person, 安全
Person person = c.get(0);
c.add(new Person()); //程式碼3,編譯錯誤
}
程式碼3
為什麼會編譯錯誤呢?
method1
只知道這個泛型的型別是Person
的子類,具體是哪一個不知道。如果程式碼3
編譯成功,那麼上述的程式碼中,就是往List<Teacher> teachers
中添加了一個Person
元素。此時,後續在操作List<Teacher> teachers
時,大概率會丟擲ClassCastException
異常。
再來看如下程式碼:
public static void test4() {
List<Person> teachers = Lists.newArrayList(new Teacher(), new Person());
// method1 處理的是 Person 的 子類,Teacher 是 Person 的子類
method2(teachers);
}
// 只知道這個泛型的型別是Teacher的父類,具體是哪一個不知道
public static void method2(List<? super Teacher> c) {
// 具體是哪一個不知道, 只能用Object接收
Object object = c.get(0); // 程式碼4
c.add(new Teacher()); // 程式碼5,不報錯
}
method2
泛型型別是Teacher
的父類,而Teacher
的父類有很多,所以程式碼4
只能使用Object
來接收。子類繼承父類,所以往集合中新增一個Teacher
物件是安全的操作。
最佳實踐:PECS 原則
PECS:producer extends, consumer super
。
- 生產者,生產資料的, 使用
<? extends T>
- 消費者,消費資料的,使用
<? super T>
怎麼理解呢?我們直接上程式碼:
/**
* producer - extends, consumer- super
*/
public static void addAll(Collection<? extends Object> producer,
Collection<? super Object> consumer) {
consumer.addAll(producer);
}
有同學可能會說,這個原則記不住怎麼辦?
沒關係,筆者有時候也記不清。不過幸運的是,在 JDK 中有這個一個方法:java.util.Collections#copy
,該方法很好的闡述了 PECS 原則。每次想用又記不清的時候,看一眼該方法就明白了~
// java.util.Collections#copy
public static <T> void copy(List<? super T> dest, List<? extends T> src){}
畫外音:知識很多、很雜,我們應該在大腦中建立索引,遇到問題,通過索引來快速查詢解決方法
更安全的泛型檢查
上述的一些檢查都是編譯時的檢查,而想要騙過編譯器的檢查也很簡單:
public static void test5() {
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
List copy = list;
copy.add("a");
List<Integer> list2 = copy;
}
test5
方法就騙過了編譯器,而且能成功執行。
那什麼時候會報錯呢?當程式去讀取list2
中的元素時,才會丟擲ClassCastException
異常。
Java 給我們提供了java.util.Collections#checkedList
方法,在呼叫add
時就會檢查型別是否匹配。
public static void test6() {
List<Integer> list = Collections.checkedList(Arrays.asList(1, 2, 3, 4, 5), Integer.class);
List copy = list;
// Exception in thread "main" java.lang.ClassCastException: Attempt to insert class java.lang.String element into collection with element type class java.lang.Integer
copy.add("a");
}
畫外音:這是一種 fail-fast 的思想,在 add 時發現型別不一致立刻報錯,而不是繼續執行可能存在問題的程式
型別擦除(Type Erasure)
我們知道,編譯器會將泛型擦除,那怎麼理解泛型擦除呢?是統一改成Object
嗎?
泛型擦除遵循以下規則:
- 如果泛型引數無界,則編譯器會將其替換為
Object
。 - 如果泛型引數有界,則編譯器會將其替換為邊界型別。
public class TypeErasureDemo {
public <T> void forEach(Collection<T> collection) {}
public <E extends String> void iter(Collection<E> collection) {}
}
使用javap
命令檢視 Class 檔案資訊:
通過 Class 檔案資訊可以看到:編譯器將forEach
方法的泛型替換為了Object
,將iter
方法的泛型替換為了String
。
泛型和方法過載(overload)
瞭解完泛型擦除規則之後,我們來看一下當泛型遇到方法過載,會遇到什麼樣的問題呢?
閱讀如下程式碼:
// 第一組
public static void printArray(Object[] objs) {}
public static <T> void printArray(T[] objs) {}
// 第二組
public static void printArray(Object[] objs) {}
public static <T extends Person> void printArray(T[] objs) {}
上面兩組方法是否都構成了過載呢?
第一組:泛型會被擦除,也就是說,在執行時期,
T[]
其實就是Object[]
,因此第一組不構成過載。第二組:
<T extends Person>
表明接收的方法是Person
的子類,構成過載。
使用 ResolvableType 解析泛型
Spring 框架中提供了org.springframework.core.ResolvableType
來優雅解析泛型。
一個簡單的使用示例如下:
public class ResolveTypeDemo {
private static final List<String> strList = Lists.newArrayList("a");
public <T extends CharSequence> void exchange(T obj) {}
public static void resolveFieldType() throws Exception {
Field field = ReflectionUtils.findField(ResolveTypeDemo.class, "strList");
ResolvableType resolvableType = ResolvableType.forField(field);
// class java.lang.String
System.out.println(resolvableType.getGeneric(0).resolve());
}
public static void resolveMethodParameterType() throws Exception {
Parameter[] parameters = ReflectionUtils.findMethod(ResolveTypeDemo.class, "exchange", CharSequence.class).getParameters();
ResolvableType resolvableType = ResolvableType.forMethodParameter(MethodParameter.forParameter(parameters[0]));
// interface java.lang.CharSequence
System.out.println(resolvableType.resolve());
}
public static void resolveInstanceType() throws Exception {
PayloadApplicationEvent<String> instance = new PayloadApplicationEvent<>(new Object(), "hi");
ResolvableType resolvableTypeForInstance = ResolvableType.forInstance(instance);
// class java.lang.String
System.out.println(resolvableTypeForInstance.as(PayloadApplicationEvent.class).getGeneric().resolve());
}
}
泛型和 JSON 反序列化
最近看到這樣一個程式碼,使用 Jackson 將 JSON 轉化為 Map。
public class JsonToMapDemo {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public static <K, V> Map<K, V> toMap(String json) throws JsonProcessingException {
return (Map) OBJECT_MAPPER.readValue(json, new TypeReference<Map<K, V>>() {
});
}
public static void main(String[] args) throws JsonProcessingException {
// {"1":{"id":1}}
String json = "{\"1\":{\"id\":1}}";
Map<Integer, User> userIdMap = OBJECT_MAPPER.readValue(json, new TypeReference<Map<Integer, User>>() {
});
userIdMap.forEach((integer, user) -> {
System.out.println(user.getId());
});
}
@Data
public static class User implements Serializable {
private static final long serialVersionUID = 8817514749356118922L;
private int id;
}
}
執行 main 方法,程式碼雖然正常結束。但是這個程式碼其實是有問題的,有什麼問題呢?一起來看如下程式碼:
public static void main(String[] args) {
// {"1":{"id":1}}
String json = "{\"1\":{\"id\":1}}";
Map<Integer, User> userIdMap = toMap(json);
userIdMap.forEach((integer, user) -> {
// 出處程式碼會報錯
// Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
System.out.println(user.getId());
});
}
為什麼會報ClassCastException
呢?讓我們來 Debug 一探究竟。
通過 Debug 可以發現:Map<Integer, User> userIdMap
物件的 key 其實是String
型別,而 value 是一個LinkedHashMap
。這很好理解,上述程式碼這個寫法,根本不知道 K,V 是什麼。正確寫法如下:
public static void main(String[] args) throws JsonProcessingException {
// {"1":{"id":1}}
String json = "{\"1\":{\"id\":1}}";
Map<Integer, User> userIdMap = OBJECT_MAPPER.readValue(json, new TypeReference<Map<Integer, User>>() {
});
userIdMap.forEach((integer, user) -> {
System.out.println(user.getId());
});
}
歡迎關注微信公眾號:Coder小黑