淺談Java中的橋接方法與泛型的逆變和協(xié)變
泛型的協(xié)變和逆變是什么?對應(yīng)于Java當中,協(xié)變對應(yīng)的就是<? extends XXX>,而逆變對應(yīng)的就是<? super XXX>。
1. 泛型的協(xié)變
1.1 泛型協(xié)變的使用
當我們有一個有方法,方法的簽名定義成為如下的方式
public static void test(List<Number> list)
這時,如果我們想要給test方法傳入一個List<Double>或者是List<Integer>可以嗎?很顯然不行,因為傳遞參數(shù),肯定是要傳遞它的子類才行,但是List<Double>或者是List<Integer>是它的子類嗎?很明顯不是,這時我們就需要用到泛型的協(xié)變。
我們將方法的參數(shù)變成如下的這種形式
public static void test(List<? extends Number> list)
這時,我們的泛型,就只需要傳入一個是Number的子類型的泛型即可。因為Integer和Double,它們都是Number的子類,因此很明顯是合法的。
test(new ArrayList<Integer>()); test(new ArrayList<Double>());
在test方法中:
- 1.如果我們想要去獲取集合當中的某個元素時,因為約定了元素的所有類型都得是Number類型極其子類的,因此我們獲取的元素一定可以用它們的共同父類Number去進行接收。
- 2.但是當我們想要往集合當中添加元素時,竟然無法往list當中添加元素?奇奇怪怪的!而且關(guān)鍵我們的list,只要求元素的類型是Number或者它的子類類型。但是我們加入的是1,是個Intger類型,很明顯是符合規(guī)范的呀!
public static void test(List<? extends Number> list) {
Number number = list.get(0); // right
list.add(1); // error
}
1.2 泛型協(xié)變存在的問題
泛型的協(xié)變,不能讓我們往集合當中添加元素。那么為什么不能添加呢?
要知道為什么,我們首先需要了解Java當中橋接方法的來由。
1.2.1 Java當中橋接方法的來由
我們首先定義如下的自定義ArrayList類,并重寫了它的add方法,
public class MyArrayList extends ArrayList<Double> {
@Override
public boolean add(Double e) {
return super.add(e);
}
}
首先,我們肯定知道ArrayList類中的add方法的原型是下面這樣的
public boolean add(E e)
在Java當中,是在編譯時去進行類型擦除的,在運行時并無泛型類型一說。也就是說,該原型方法,會被抹掉成為
public boolean add(Object e)
但是,我們定義了自己的ArrayList,我們自己的add方法的原型為
public boolean add(Double e)
這個兩個方法的簽名并不相同,但是當使用下的代碼創(chuàng)建一個ArrayList時:
ArrayList<Double> list = new MyArrayList(); list.add(1.0);
它實際調(diào)用的方法的原型是public boolean add(Object e),但是我們子類中的重寫的方法的原型時什么?public booleab add(Double e)。
也就是說,通過父類的方法調(diào)用的和子類重寫的方法,并不是同一個方法,因為它們連方法簽名都不同。這時候,就需要要一個方式,將public booleab add(Object e)轉(zhuǎn)到public booleab add(Double e)當中去執(zhí)行。這時候,就會涉及到橋接方法的存在了。
Java的實現(xiàn)方式是:通過在Javac編譯器編譯時,為我們生成一個public boolean add(Object e)這樣的方法,而這個方法當中,要做的實際上就是調(diào)用public booleab add(Double e)這個方法。
public boolean add(Object o) {
return add((Double) o);
}
通過橋接方法的方式,就可以讓我們能在針對泛型方法進行重寫時,可以被JVM執(zhí)行到。
1.2.2 為什么泛型協(xié)變時,不允許添加元素呢
當我們使用下面的代碼創(chuàng)建了一個我們自定義的MyArrayList對象。
ArrayList<Double> list = new MyArrayList();
這時,我們調(diào)用test方法
test(list)
test方法對于list的泛型定義為<? entends Number>,理論上應(yīng)該是可以往里面放入任何Number子類類型的元素的。但是別忘了,我們MyArrayList中對于方法的定義,是下面這樣子的!
public boolean add(Object e) {
return add((Double)e);
}
public boolean add(Double e) {
// ......
}
如果我們往集合當中添加一個Integer類型的1,走到橋接方法當中時會有(Double)e這樣的強制類型轉(zhuǎn)換,這不就是拋出了ClassCastException異常了嗎?很明顯,是不允許我們這樣干的。因此Java的做法就是,在編譯期就去禁止這種做法,避免產(chǎn)生運行時的ClassCastException。
有的人也許會說
ArrayList<Double> list = new MyArrayList();
我們創(chuàng)建list時,不是約束了泛型類型為Double了嗎,為什么test方法內(nèi)就不能默認它是Double的泛型呢?問題就是:我寫test方法時,我怎么知道你傳遞的是Double類型的泛型,玩意別人傳遞的是Integer的泛型呢?所以很明顯是行不通的。
1.2.3 從Java字節(jié)碼的角度去看橋接方法


我們可以看到,Javac編譯器,在對Java代碼進行編譯時,其實針對add方法去生成了兩個方法,而它們的訪問標識符并不相同。我們自己的方法的訪問標識符為0x0001[public],而Javac編譯器為我們生成的橋接方法的返回值,為0x1041[pubic synthetic bridge],多了兩個訪問標識符synthetic和bridge。
我們打開橋接方法的code字節(jié)碼
我們來分析下字節(jié)碼
- 1.
aload_0,眾所周知,就是從LocalVariableTable(局部變量表)獲取this對象的引用,并壓棧。 - 2.
aload_1,自然就是將傳入的元素e的引用壓棧。 - 3.
checkcast #3 <java/lang/Double>,自然是檢查能否執(zhí)行強制類型轉(zhuǎn)換。 - 4.
invokevirtual #4 <com/wanna/generics/java/MyArrayList.add : (Ljava/lang/Double;)Z>,做到實際上就是從常量池的4號元素當中拿到要執(zhí)行的方法,也就是我們自己實現(xiàn)的方法。invokevirtual就是執(zhí)行目標方法,沒毛病。 - 5.
ireturn,自然就是返回一個int類型的值,為什么是int類型?而不是boolean類型?因為Java當中,在存放到局部變量表和棧中的情況下,int/byte/boolean/char,都是使用的int的形式存放的,占用一個局部變量表的槽位。
我們通過分析得到的信息和我們之前的分析一致,就是通過橋接方法橋接一下,去調(diào)用我們自己實現(xiàn)的方法。我們接下來,嘗試使用反射的方式去獲取到add方法有幾個,方法信息是什么。

Arrays.stream(MyArrayList.class.getMethods()).filter(method -> method.getName().equals("add") && method.getParameterCount() == 1).forEach(method -> {
System.out.printf("方法名為:%s,方法的返回值類型為:%s,方法的參數(shù)列表為:%s%n",
method.getName(), method.getReturnType(), Arrays.toString(method.getParameterTypes()));
});
代碼的最終執(zhí)行結(jié)果為
方法名為:add,方法的返回值類型為:boolean,方法的參數(shù)列表為:[class java.lang.Double]
方法名為:add,方法的返回值類型為:boolean,方法的參數(shù)列表為:[class java.lang.Object]
也就是說,生成的橋接方法,是我們可以通過反射拿到的,它是一個真實的方法。
通過反射拿到Method之后,我們還可以通過訪問標識符判斷該方法是否是橋接方法。
method.isBridge() method.isSynthetic()
判斷橋接方法,實際上,在Spring框架當中的反射工具類(ReflectionUtils)當中就有用到,用來判斷一個方法是否是用戶定義的方法。

2. 泛型逆變
2.1 泛型逆變的使用
泛型逆變的泛型形式是:<? super XXX>,它的作用是賦值給它的約束容器的泛型類型,只能是XXX以及它的父類。
那么我們可以往容器里放入它的子類嗎?也許會說,上面不是都說了需要放入的是XXX以及它的父類嗎,那肯定是不能放入它的子類的呀!但是我們需要想到一個問題,那就是XXX的所有子類,其實都是可以隱式轉(zhuǎn)換為XXX類型,或者可以直接說,它的子類就是XXX類型。
我們依次定義三個類
static class Person {
}
static class User extends Person {
}
static class Student extends User {
}
接著,定義一個使用逆變的泛型參數(shù)的方法
public static void test(List<? super User> list)
上面我們說了,可以接收的容器泛型類型是User以及它的父類,也就是說,容器的泛型可以是User也基于是Person。因此,我們可以傳入下面這樣的容器給test方法。
test(new ArrayList<Person>());
在test方法當中,我們可以執(zhí)行下面的才做
list.add(new User()); // 放入User list.add(new Student()); // 放入User的子類
2.2 泛型逆變會有什么問題
我們需要想想一個問題:我們使用了逆變約定了,接收的容器的泛型類型是User以及User的父類。我們往容器當中放入的元素,可以是User以及User的子類。也就是說,我們獲取容器中的元素時,根本不知道是什么類型,只能用Object去接收從容器中獲取的元素類型,因為只是約定了容器的泛型為User和User的父類,而Object也是它的父類,因此我們甚至可以傳入一個容器類型為ArrayList<Object>,我們根本無法決定元素類型的上限,只能用Object去進行接收。
final Object object = list.get(0);
現(xiàn)在又有一個問題:之前協(xié)變時,會出現(xiàn)因為執(zhí)行橋接方法時,發(fā)生類型轉(zhuǎn)換異常,在逆變當中會出現(xiàn)這種情況嗎?
我們仔細想想,接收的容器泛型類型為User以及User的父類,而可以往容器里存放的是User以及User的子類,也就是說,我們放入到容器中的元素類型,比你原來約束的類型還嚴格,因為:"User以及User的子類"一定是"User以及User的父類"的子類。也就是說,逆變當中,并不會因為橋接方法中進行的類型導(dǎo)致ClassCastException,所以允許add。
3.協(xié)變與逆變-PECS原則
對于協(xié)變和逆變,有這樣的一個原則:稱為PECS(Producer Extends Consumer Super)。也就是說:
- 1.Extends應(yīng)該用在生產(chǎn)者的情況,也就是要根據(jù)泛型類型去返回對象的形式。
- 2.Super應(yīng)該用在消費者的情況,應(yīng)該傳入一個泛型類型的容器,應(yīng)該利用該容器對數(shù)據(jù)進行處理,但是不能根據(jù)泛型去進行返回,如果要進行返回,只能返回Object,但是這就失去了泛型的意義。
public static <T> void testCS(List<? super T> list) { // Consumer Super
list.add(...);
}
public static <T> T testPE(List<? extends T> list) { // Producer Extends
return list.get(0);
}到此這篇關(guān)于淺談Java中的橋接方法與泛型的逆變和協(xié)變的文章就介紹到這了,更多相關(guān)Java橋接方法與泛型逆變協(xié)變內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JAVA 多態(tài)操作----父類與子類轉(zhuǎn)換問題實例分析
這篇文章主要介紹了JAVA 多態(tài)操作----父類與子類轉(zhuǎn)換問題,結(jié)合實例形式分析了JAVA 多態(tài)操作中父類與子類轉(zhuǎn)換問題相關(guān)原理、操作技巧與注意事項,需要的朋友可以參考下2020-05-05
spring boot task實現(xiàn)動態(tài)創(chuàng)建定時任務(wù)的方法
這篇文章主要介紹了spring boot task實現(xiàn)動態(tài)創(chuàng)建定時任務(wù),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-01-01
EasyExcel工具讀取Excel空數(shù)據(jù)行問題的解決辦法
EasyExcel是阿里巴巴開源的一個excel處理框架,以使用簡單,節(jié)省內(nèi)存著稱,下面這篇文章主要給大家介紹了關(guān)于EasyExcel工具讀取Excel空數(shù)據(jù)行問題的解決辦法,需要的朋友可以參考下2022-08-08

