<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
泛型的協變和逆變是什麼?對應於Java當中,協變對應的就是<? extends XXX>
,而逆變對應的就是<? super XXX>
。
當我們有一個有方法,方法的簽名定義成為如下的方式
public static void test(List<Number> list)
這時,如果我們想要給test方法傳入一個List<Double>
或者是List<Integer>
可以嗎?很顯然不行,因為傳遞引數,肯定是要傳遞它的子類才行,但是List<Double>
或者是List<Integer>
是它的子類嗎?很明顯不是,這時我們就需要用到泛型的協變。
我們將方法的引數變成如下的這種形式
public static void test(List<? extends Number> list)
這時,我們的泛型,就只需要傳入一個是Number的子型別的泛型即可。因為Integer和Double,它們都是Number的子類,因此很明顯是合法的。
test(new ArrayList<Integer>()); test(new ArrayList<Double>());
在test方法中:
public static void test(List<? extends Number> list) { Number number = list.get(0); // right list.add(1); // error }
泛型的協變,不能讓我們往集合當中新增元素。那麼為什麼不能新增呢?
要知道為什麼,我們首先需要了解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)
這個兩個方法的簽名並不相同,但是當使用下的程式碼建立一個ArrayList時:
ArrayList<Double> list = new MyArrayList(); list.add(1.0);
它實際呼叫的方法的原型是public boolean add(Object e)
,但是我們子類中的重寫的方法的原型時什麼?public booleab add(Double e)
。
也就是說,通過父類別的方法呼叫的和子類重寫的方法,並不是同一個方法,因為它們連方法簽名都不同。這時候,就需要要一個方式,將public booleab add(Object e)
轉到public booleab add(Double e)
當中去執行。這時候,就會涉及到橋接方法的存在了。
Java的實現方式是:通過在Javac編譯器編譯時,為我們生成一個public boolean add(Object e)
這樣的方法,而這個方法當中,要做的實際上就是呼叫public booleab add(Double e)
這個方法。
public boolean add(Object o) { return add((Double) o); }
通過橋接方法的方式,就可以讓我們能在針對泛型方法進行重寫時,可以被JVM執行到。
當我們使用下面的程式碼建立了一個我們自定義的MyArrayList物件。
ArrayList<Double> list = new MyArrayList();
這時,我們呼叫test方法
test(list)
test方法對於list的泛型定義為<? entends Number>
,理論上應該是可以往裡面放入任何Number
子類型別的元素的。但是別忘了,我們MyArrayList中對於方法的定義,是下面這樣子的!
public boolean add(Object e) { return add((Double)e); } public boolean add(Double e) { // ...... }
如果我們往集合當中新增一個Integer型別的1,走到橋接方法當中時會有(Double)e
這樣的強制型別轉換,這不就是丟擲了ClassCastException
異常了嗎?很明顯,是不允許我們這樣乾的。因此Java的做法就是,在編譯期就去禁止這種做法,避免產生執行時的ClassCastException
。
有的人也許會說
ArrayList<Double> list = new MyArrayList();
我們建立list時,不是約束了泛型型別為Double
了嗎,為什麼test方法內就不能預設它是Double的泛型呢?問題就是:我寫test方法時,我怎麼知道你傳遞的是Double
型別的泛型,玩意別人傳遞的是Integer的泛型呢?所以很明顯是行不通的。
我們可以看到,Javac編譯器,在對Java程式碼進行編譯時,其實針對add方法去生成了兩個方法,而它們的存取識別符號並不相同。我們自己的方法的存取識別符號為0x0001[public]
,而Javac編譯器為我們生成的橋接方法的返回值,為0x1041[pubic synthetic bridge]
,多了兩個存取識別符號synthetic
和bridge
。
我們開啟橋接方法的code位元組碼
我們來分析下位元組碼
aload_0
,眾所周知,就是從LocalVariableTable(區域性變數表)獲取this物件的參照,並壓棧。aload_1
,自然就是將傳入的元素e的參照壓棧。checkcast #3 <java/lang/Double>
,自然是檢查能否執行強制型別轉換。invokevirtual #4 <com/wanna/generics/java/MyArrayList.add : (Ljava/lang/Double;)Z>
,做到實際上就是從常數池的4號元素當中拿到要執行的方法,也就是我們自己實現的方法。invokevirtual
就是執行目標方法,沒毛病。ireturn
,自然就是返回一個int型別的值,為什麼是int型別?而不是boolean型別?因為Java當中,在存放到區域性變數表和棧中的情況下,int/byte/boolean/char,都是使用的int的形式存放的,佔用一個區域性變數表的槽位。我們通過分析得到的資訊和我們之前的分析一致,就是通過橋接方法橋接一下,去呼叫我們自己實現的方法。我們接下來,嘗試使用反射的方式去獲取到add方法有幾個,方法資訊是什麼。
Arrays.stream(MyArrayList.class.getMethods()).filter(method -> method.getName().equals("add") && method.getParameterCount() == 1).forEach(method -> { System.out.printf("方法名為:%s,方法的返回值型別為:%s,方法的參數列為:%s%n", method.getName(), method.getReturnType(), Arrays.toString(method.getParameterTypes())); });
程式碼的最終執行結果為
方法名為:add,方法的返回值型別為:boolean,方法的參數列為:[class java.lang.Double]
方法名為:add,方法的返回值型別為:boolean,方法的參數列為:[class java.lang.Object]
也就是說,生成的橋接方法,是我們可以通過反射拿到的,它是一個真實的方法。
通過反射拿到Method
之後,我們還可以通過存取識別符號判斷該方法是否是橋接方法。
method.isBridge() method.isSynthetic()
判斷橋接方法,實際上,在Spring框架當中的反射工具類(ReflectionUtils
)當中就有用到,用來判斷一個方法是否是使用者定義的方法。
泛型逆變的泛型形式是:<? super XXX>
,它的作用是賦值給它的約束容器的泛型型別,只能是XXX
以及它的父類別。
那麼我們可以往容器裡放入它的子類嗎?也許會說,上面不是都說了需要放入的是XXX
以及它的父類別嗎,那肯定是不能放入它的子類的呀!但是我們需要想到一個問題,那就是XXX
的所有子類,其實都是可以隱式轉換為XXX
型別,或者可以直接說,它的子類就是XXX
型別。
我們依次定義三個類
static class Person { } static class User extends Person { } static class Student extends User { }
接著,定義一個使用逆變的泛型引數的方法
public static void test(List<? super User> list)
上面我們說了,可以接收的容器泛型型別是User以及它的父類別,也就是說,容器的泛型可以是User也基於是Person。因此,我們可以傳入下面這樣的容器給test方法。
test(new ArrayList<Person>());
在test方法當中,我們可以執行下面的才做
list.add(new User()); // 放入User list.add(new Student()); // 放入User的子類
我們需要想想一個問題:我們使用了逆變約定了,接收的容器的泛型型別是User以及User的父類別。我們往容器當中放入的元素,可以是User以及User的子類。也就是說,我們獲取容器中的元素時,根本不知道是什麼型別,只能用Object去接收從容器中獲取的元素型別,因為只是約定了容器的泛型為User和User的父類別,而Object也是它的父類別,因此我們甚至可以傳入一個容器型別為ArrayList<Object>
,我們根本無法決定元素型別的上限,只能用Object去進行接收。
final Object object = list.get(0);
現在又有一個問題:之前協變時,會出現因為執行橋接方法時,發生型別轉換異常,在逆變當中會出現這種情況嗎?
我們仔細想想,接收的容器泛型型別為User以及User的父類別,而可以往容器裡存放的是User以及User的子類,也就是說,我們放入到容器中的元素型別,比你原來約束的型別還嚴格,因為:"User以及User的子類"一定是"User以及User的父類別"的子類。也就是說,逆變當中,並不會因為橋接方法中進行的型別導致ClassCastException
,所以允許add。
對於協變和逆變,有這樣的一個原則:稱為PECS(Producer Extends Consumer Super)。也就是說:
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); }
到此這篇關於淺談Java中的橋接方法與泛型的逆變和協變的文章就介紹到這了,更多相關Java橋接方法與泛型逆變協變內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45