?歡迎關注我的公眾號“彤哥讀原始碼”,檢視更多原始碼系列文章, 與彤哥一起暢遊原始碼的海洋。
背景
昨天,在逛論壇時遇到個這麼個問題,上程式碼:
public class GenericTest {
//方法一
public static <T extends Comparable<T>> List<T> sort(List<T> list) {
return Arrays.asList(list.toArray((T[]) new Comparable[list.size()]));
}
//方法二
public static <T extends Comparable<T>> T[] sort2(List<T> list) {
// 這裡沒報錯
return list.toArray((T[]) new Comparable[list.size()]);
}
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
// 方法一呼叫正常
System.out.println(sort(list).getClass());
// 方法二呼叫報錯了,這裡報錯了
System.out.println(sort2(list).getClass());
}
}
複製程式碼
這個問題有以下四個現象:
(1)方法一呼叫完全正常;
(2)方法二呼叫報錯了;
(3)方法二報錯的地方是在System.out.println(sort2(list).getClass());
這行,而不是return list.toArray((T[]) new Comparable[list.size()]);
這行;
(4)報的錯是[Ljava.lang.Comparable; cannot be cast to [Ljava.lang.Integer;
;
怎麼樣?你心中有答案嘛?型別擦除?怎麼擦?摩擦摩擦?
解決
剛拿到這道題,我也是一臉懵逼,這要報錯也應該是在return list.toArray((T[]) new Comparable[list.size()]);
這行啊,而且要報錯應該兩個方法都報錯啊。
抱著不放棄不拋棄的心態,彤哥做了大量的實驗,終於得出了泛型的本質,且聽我娓娓道來。
小插曲
首先,我們要明白,java中的陣列是不支援向下轉型的,但是如果本身就是那個型別的是可以轉過去的,請看下面的例子:
public static void main(String[] args) {
Object[] objs = new Object[]{1};
// 型別轉換錯誤
// Integer[] ins = (Integer[]) objs;
Object[] objs2 = new Integer[]{1};
// 不報錯
Integer[] ins2 = (Integer[]) objs2;
}
複製程式碼
型別擦除
java裡的泛型是假泛型,只在編譯期有效,在執行時是沒有泛型的概念的,舉個簡單的例子:
public static void main(String[] args) {
List<String> strList = Arrays.asList("1");
List<Integer> intList = Arrays.asList(1);
// 列印:true
System.out.println(strList.getClass() == intList.getClass());
}
複製程式碼
可以看到兩個list的型別是一樣的,如果你覺得這個例子不夠說服力,那我給你個過分點的例子:
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
List<String> strList = new ArrayList<>();
Method addMethod = strList.getClass().getMethod("add", Object.class);
addMethod.invoke(strList, 1);
addMethod.invoke(strList, true);
addMethod.invoke(strList, new Long(1));
addMethod.invoke(strList, new Byte[]{1});
// 列印:[1, true, 1, 1]
System.out.println(strList);
}
複製程式碼
瞧,我可以往一個String型別的List中扔任何我想扔的東西,服不服?!
所以說java裡面的泛型是假的,執行時不存在滴。
迴歸正題
陣列不能向下強轉我懂了,型別擦除我也懂了,似乎還是過不好這一生,呃不是,是還是解決不了這道題啊?
呃,好像是~~
我們再來看一個簡單的例子:
// GenericTest2.java(原始碼)
public class GenericTest2 {
public static void main(String[] args) {
System.out.println(raw("1"));
}
public static <T> T raw(T t) {
return t;
}
}
// GenericTest2.class(反編譯)
public class GenericTest2 {
public GenericTest2() {
}
public static void main(String[] args) {
System.out.println((String)raw("1"));
}
public static <T> T raw(T t) {
return t;
}
}
複製程式碼
嗯~似乎看出來點端倪,反編譯後多了個構造方法。
呃,沒錯。還有呢?
仔細一看,System.out.println((String)raw("1"));
這一句多加了個String強轉。
這就是關鍵所在,結合型別擦除,執行時並沒有所謂的泛型,所以raw()返回的其實是Object,但是呼叫者自己知道我要的是String型別啊,所以我就知道強轉一下嘍。
我們再來看個極端的例子:
// GenericTest2.java(原始碼)
public class GenericTest2 {
public static void main(String[] args) {
System.out.println(raw("1"));
}
public static <T> T raw(T t) {
return (T)new Integer(1);
}
}
// GenericTest2.class(反編譯)
public class GenericTest2 {
public GenericTest2() {
}
public static void main(String[] args) {
System.out.println((String)raw("1"));
}
public static <T> T raw(T t) {
return new Integer(1);
}
}
複製程式碼
仔細觀察,可以發現,raw()方法裡的強轉(T)new Integer(1)
變成了new Integer(1)
,強轉被擦除了,實際上在執行時這裡的T變成了Object,所有型別都是Object的子類,也就不需要強轉了。
而(String)raw("1")
的強轉還是加上的,這是呼叫者知道型別是String,所以raw()返回後自己強轉成String一下。
當然,這個程式碼執行是會報錯的,java.lang.Integer cannot be cast to java.lang.String
,因為raw()返回的是Integer型別,強轉成String型別失敗了。
好了,基本思路就是這樣。
泛型類呢?
我們上面舉的例子都是泛型方法,那麼泛型類呢?
同樣地,我們來看個例子:
// GenericTest3.java(原始碼)
public class GenericTest3 {
public static void main(String[] args) {
System.out.println(new Raw<String>().raw("1"));
}
}
class Raw<T> {
public T raw(T t) {
return (T)new Integer(1);
}
}
// GenericTest3.class(反編譯)
public class GenericTest3 {
public GenericTest3() {
}
public static void main(String[] args) {
System.out.println((String)(new Raw()).raw("1"));
}
}
class Raw<T> {
Raw() {
}
public T raw(T t) {
return new Integer(1);
}
}
複製程式碼
可以看到,跟泛型方法的表現一模一樣。當然,這裡執行時也會報java.lang.Integer cannot be cast to java.lang.String
這個錯誤。
總結
java中的泛型只在編譯期有效,在執行時只有呼叫者知道需要什麼型別,且呼叫者呼叫泛型方法後自己做強制轉換,被呼叫者是完全無感的。
所以,出現問題不要問被呼叫者,而是要問呼叫者,你丫是怎麼呼叫的?!
解答開篇
為了方便我們還是把開篇的問題拿過來。
// GenericTest.java(原始碼)
public class GenericTest {
//方法一
public static <T extends Comparable<T>> List<T> sort(List<T> list) {
return Arrays.asList(list.toArray((T[]) new Comparable[list.size()]));
}
//方法二
public static <T extends Comparable<T>> T[] sort2(List<T> list) {
// 這裡沒報錯
return list.toArray((T[]) new Comparable[list.size()]);
}
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
// 方法一呼叫正常
System.out.println(sort(list).getClass());
// 方法二呼叫報錯了,這裡報錯了
System.out.println(sort2(list).getClass());
}
}
複製程式碼
這裡似乎又不太一樣,變成了<T extends Comparable<T>>
,其實是一樣的啦,如果單獨寫<T>
是相當於<T extends Object>
的。
那麼,我們就延伸一下,被呼叫者是完全無感的,它只能盡力拿到它知道的型別,比如這裡就只能盡力拿到Comparable,如果是<T>
拿到的就是Object。
所以,方法二返回的就是實打實的Comparable[]型別,作為被呼叫者,它一點問題都沒有。
但是,呼叫方是知道我需要的是Integer[]型別的,因為list裡面是Integer型別,所以返回的應該是Integer[]型別,所以我就強轉嘍,然後就報錯了。
到底是不是這樣?我們來看看反編譯後的程式碼:
// GenericTest.class(反編譯)
public class GenericTest {
public GenericTest() {
}
public static <T extends Comparable<T>> List<T> sort(List<T> list) {
return Arrays.asList(list.toArray((Comparable[])(new Comparable[list.size()])));
}
public static <T extends Comparable<T>> T[] sort2(List<T> list) {
// 這裡使用的是Comparable[]強轉,所以返回的也是實打實的Comparable[]型別
return (Comparable[])list.toArray((Comparable[])(new Comparable[list.size()]));
}
public static void main(String[] args) {
List<Integer> list = new ArrayList();
list.add(1);
list.add(2);
System.out.println(sort(list).getClass());
// 陣列向下轉型失敗
System.out.println(((Integer[])sort2(list)).getClass());
}
}
複製程式碼
可以看到,跟我們的分析完全一致。
一句話,一輩子
java中的泛型只在編譯期有效,在執行時只有呼叫者知道它自己需要什麼型別,且呼叫者呼叫泛型方法後自己做強制轉換,被呼叫者是完全無感的,被呼叫者只能盡力拿到它所知道的型別。
此時,我的腦海中不經響起那熟悉的旋律,“一句話,一輩子……”,今天的這句話你記住了嗎?
歡迎關注我的公眾號“彤哥讀原始碼”,檢視更多原始碼系列文章, 與彤哥一起暢遊原始碼的海洋。