欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

淺談Java中的橋接方法與泛型的逆變和協(xié)變

 更新時(shí)間:2022年04月07日 10:57:51   作者:Wannay  
對應(yīng)于Java當(dāng)中,協(xié)變對應(yīng)的就是<? extends XXX>,而逆變對應(yīng)的就是<? super XXX>,本文詳細(xì)的介紹了Java中的橋接方法與泛型的逆變和協(xié)變,感興趣的可以了解一下

泛型的協(xié)變和逆變是什么?對應(yīng)于Java當(dāng)中,協(xié)變對應(yīng)的就是<? extends XXX>,而逆變對應(yīng)的就是<? super XXX>。

1. 泛型的協(xié)變

1.1 泛型協(xié)變的使用

當(dāng)我們有一個(gè)有方法,方法的簽名定義成為如下的方式

public static void test(List<Number> list)

這時(shí),如果我們想要給test方法傳入一個(gè)List<Double>或者是List<Integer>可以嗎?很顯然不行,因?yàn)閭鬟f參數(shù),肯定是要傳遞它的子類才行,但是List<Double>或者是List<Integer>是它的子類嗎?很明顯不是,這時(shí)我們就需要用到泛型的協(xié)變。

我們將方法的參數(shù)變成如下的這種形式

public static void test(List<? extends Number> list)

這時(shí),我們的泛型,就只需要傳入一個(gè)是Number的子類型的泛型即可。因?yàn)镮nteger和Double,它們都是Number的子類,因此很明顯是合法的。

test(new ArrayList<Integer>());
test(new ArrayList<Double>());

在test方法中:

  • 1.如果我們想要去獲取集合當(dāng)中的某個(gè)元素時(shí),因?yàn)榧s定了元素的所有類型都得是Number類型極其子類的,因此我們獲取的元素一定可以用它們的共同父類Number去進(jìn)行接收。
  • 2.但是當(dāng)我們想要往集合當(dāng)中添加元素時(shí),竟然無法往list當(dāng)中添加元素?奇奇怪怪的!而且關(guān)鍵我們的list,只要求元素的類型是Number或者它的子類類型。但是我們加入的是1,是個(gè)Intger類型,很明顯是符合規(guī)范的呀!
    public static void test(List<? extends Number> list) {
        Number number = list.get(0);  // right
        list.add(1); // error
    }

1.2 泛型協(xié)變存在的問題

泛型的協(xié)變,不能讓我們往集合當(dāng)中添加元素。那么為什么不能添加呢?

要知道為什么,我們首先需要了解Java當(dāng)中橋接方法的來由。

1.2.1 Java當(dāng)中橋接方法的來由

我們首先定義如下的自定義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當(dāng)中,是在編譯時(shí)去進(jìn)行類型擦除的,在運(yùn)行時(shí)并無泛型類型一說。也就是說,該原型方法,會被抹掉成為

public boolean add(Object e) 

但是,我們定義了自己的ArrayList,我們自己的add方法的原型為

public boolean add(Double e) 

這個(gè)兩個(gè)方法的簽名并不相同,但是當(dāng)使用下的代碼創(chuàng)建一個(gè)ArrayList時(shí):

ArrayList<Double> list = new MyArrayList();
list.add(1.0);

它實(shí)際調(diào)用的方法的原型是public boolean add(Object e),但是我們子類中的重寫的方法的原型時(shí)什么?public booleab add(Double e)

也就是說,通過父類的方法調(diào)用的和子類重寫的方法,并不是同一個(gè)方法,因?yàn)樗鼈冞B方法簽名都不同。這時(shí)候,就需要要一個(gè)方式,將public booleab add(Object e)轉(zhuǎn)到public booleab add(Double e)當(dāng)中去執(zhí)行。這時(shí)候,就會涉及到橋接方法的存在了。

Java的實(shí)現(xiàn)方式是:通過在Javac編譯器編譯時(shí),為我們生成一個(gè)public boolean add(Object e)這樣的方法,而這個(gè)方法當(dāng)中,要做的實(shí)際上就是調(diào)用public booleab add(Double e)這個(gè)方法。

    public boolean add(Object o) {
        return add((Double) o);
    }

通過橋接方法的方式,就可以讓我們能在針對泛型方法進(jìn)行重寫時(shí),可以被JVM執(zhí)行到。

1.2.2 為什么泛型協(xié)變時(shí),不允許添加元素呢

當(dāng)我們使用下面的代碼創(chuàng)建了一個(gè)我們自定義的MyArrayList對象。

ArrayList<Double> list = new MyArrayList();

這時(shí),我們調(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)  {
    // ......
}

如果我們往集合當(dāng)中添加一個(gè)Integer類型的1,走到橋接方法當(dāng)中時(shí)會有(Double)e這樣的強(qiáng)制類型轉(zhuǎn)換,這不就是拋出了ClassCastException異常了嗎?很明顯,是不允許我們這樣干的。因此Java的做法就是,在編譯期就去禁止這種做法,避免產(chǎn)生運(yùn)行時(shí)的ClassCastException

有的人也許會說

ArrayList<Double> list = new MyArrayList();

我們創(chuàng)建list時(shí),不是約束了泛型類型為Double了嗎,為什么test方法內(nèi)就不能默認(rèn)它是Double的泛型呢?問題就是:我寫test方法時(shí),我怎么知道你傳遞的是Double類型的泛型,玩意別人傳遞的是Integer的泛型呢?所以很明顯是行不通的。

1.2.3 從Java字節(jié)碼的角度去看橋接方法

我們可以看到,Javac編譯器,在對Java代碼進(jìn)行編譯時(shí),其實(shí)針對add方法去生成了兩個(gè)方法,而它們的訪問標(biāo)識符并不相同。我們自己的方法的訪問標(biāo)識符為0x0001[public],而Javac編譯器為我們生成的橋接方法的返回值,為0x1041[pubic synthetic bridge],多了兩個(gè)訪問標(biāo)識符syntheticbridge。

我們打開橋接方法的code字節(jié)碼

 

我們來分析下字節(jié)碼

  • 1.aload_0,眾所周知,就是從LocalVariableTable(局部變量表)獲取this對象的引用,并壓棧。
  • 2.aload_1,自然就是將傳入的元素e的引用壓棧。
  • 3.checkcast #3 <java/lang/Double>,自然是檢查能否執(zhí)行強(qiáng)制類型轉(zhuǎn)換。
  • 4.invokevirtual #4 <com/wanna/generics/java/MyArrayList.add : (Ljava/lang/Double;)Z>,做到實(shí)際上就是從常量池的4號元素當(dāng)中拿到要執(zhí)行的方法,也就是我們自己實(shí)現(xiàn)的方法。invokevirtual就是執(zhí)行目標(biāo)方法,沒毛病。
  • 5.ireturn,自然就是返回一個(gè)int類型的值,為什么是int類型?而不是boolean類型?因?yàn)镴ava當(dāng)中,在存放到局部變量表和棧中的情況下,int/byte/boolean/char,都是使用的int的形式存放的,占用一個(gè)局部變量表的槽位。

我們通過分析得到的信息和我們之前的分析一致,就是通過橋接方法橋接一下,去調(diào)用我們自己實(shí)現(xiàn)的方法。我們接下來,嘗試使用反射的方式去獲取到add方法有幾個(gè),方法信息是什么。

        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]

也就是說,生成的橋接方法,是我們可以通過反射拿到的,它是一個(gè)真實(shí)的方法。

通過反射拿到Method之后,我們還可以通過訪問標(biāo)識符判斷該方法是否是橋接方法。

method.isBridge() 
method.isSynthetic()

判斷橋接方法,實(shí)際上,在Spring框架當(dāng)中的反射工具類(ReflectionUtils)當(dāng)中就有用到,用來判斷一個(gè)方法是否是用戶定義的方法。

2. 泛型逆變

2.1 泛型逆變的使用

泛型逆變的泛型形式是:<? super XXX>,它的作用是賦值給它的約束容器的泛型類型,只能是XXX以及它的父類。

那么我們可以往容器里放入它的子類嗎?也許會說,上面不是都說了需要放入的是XXX以及它的父類嗎,那肯定是不能放入它的子類的呀!但是我們需要想到一個(gè)問題,那就是XXX的所有子類,其實(shí)都是可以隱式轉(zhuǎn)換為XXX類型,或者可以直接說,它的子類就是XXX類型。

我們依次定義三個(gè)類

    static class Person {

    }

    static class User extends Person {

    }

    static class Student extends User {

    }

接著,定義一個(gè)使用逆變的泛型參數(shù)的方法

public static void test(List<? super User> list)

上面我們說了,可以接收的容器泛型類型是User以及它的父類,也就是說,容器的泛型可以是User也基于是Person。因此,我們可以傳入下面這樣的容器給test方法。

 test(new ArrayList<Person>());

在test方法當(dāng)中,我們可以執(zhí)行下面的才做

list.add(new User()); // 放入U(xiǎn)ser
list.add(new Student());  // 放入U(xiǎn)ser的子類

2.2 泛型逆變會有什么問題

我們需要想想一個(gè)問題:我們使用了逆變約定了,接收的容器的泛型類型是User以及User的父類。我們往容器當(dāng)中放入的元素,可以是User以及User的子類。也就是說,我們獲取容器中的元素時(shí),根本不知道是什么類型,只能用Object去接收從容器中獲取的元素類型,因?yàn)橹皇羌s定了容器的泛型為User和User的父類,而Object也是它的父類,因此我們甚至可以傳入一個(gè)容器類型為ArrayList<Object>,我們根本無法決定元素類型的上限,只能用Object去進(jìn)行接收。

final Object object = list.get(0);

現(xiàn)在又有一個(gè)問題:之前協(xié)變時(shí),會出現(xiàn)因?yàn)閳?zhí)行橋接方法時(shí),發(fā)生類型轉(zhuǎn)換異常,在逆變當(dāng)中會出現(xiàn)這種情況嗎?

我們仔細(xì)想想,接收的容器泛型類型為User以及User的父類,而可以往容器里存放的是User以及User的子類,也就是說,我們放入到容器中的元素類型,比你原來約束的類型還嚴(yán)格,因?yàn)椋?quot;User以及User的子類"一定是"User以及User的父類"的子類。也就是說,逆變當(dāng)中,并不會因?yàn)闃蚪臃椒ㄖ羞M(jìn)行的類型導(dǎo)致ClassCastException,所以允許add。

3.協(xié)變與逆變-PECS原則

對于協(xié)變和逆變,有這樣的一個(gè)原則:稱為PECS(Producer Extends Consumer Super)。也就是說:

  • 1.Extends應(yīng)該用在生產(chǎn)者的情況,也就是要根據(jù)泛型類型去返回對象的形式。
  • 2.Super應(yīng)該用在消費(fèi)者的情況,應(yīng)該傳入一個(gè)泛型類型的容器,應(yīng)該利用該容器對數(shù)據(jù)進(jìn)行處理,但是不能根據(jù)泛型去進(jìn)行返回,如果要進(jìn)行返回,只能返回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)文章

  • Spring Boot配置AOP打印日志的全過程

    Spring Boot配置AOP打印日志的全過程

    這篇文章主要給大家介紹了關(guān)于Spring Boot配置AOP打印日志的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用Spring Boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-08-08
  • java中Collections.sort排序函數(shù)用法詳解

    java中Collections.sort排序函數(shù)用法詳解

    本篇文章主要介紹了java中Collections.sort排序函數(shù)用法詳解,非常具有實(shí)用價(jià)值,需要的朋友可以參考下。
    2016-12-12
  • java中實(shí)現(xiàn)map與對象相互轉(zhuǎn)換的幾種實(shí)現(xiàn)

    java中實(shí)現(xiàn)map與對象相互轉(zhuǎn)換的幾種實(shí)現(xiàn)

    這篇文章主要介紹了java中實(shí)現(xiàn)map與對象相互轉(zhuǎn)換的幾種實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-07-07
  • eclipse配置tomcat10的詳細(xì)步驟總結(jié)

    eclipse配置tomcat10的詳細(xì)步驟總結(jié)

    今天給大家?guī)淼氖顷P(guān)于Java的相關(guān)知識,文章圍繞著eclipse配置tomcat10的詳細(xì)步驟展開,文中有非常詳細(xì)的介紹及圖文示例,需要的朋友可以參考下
    2021-06-06
  • 淺談java對象轉(zhuǎn)json,數(shù)字精確出現(xiàn)丟失問題

    淺談java對象轉(zhuǎn)json,數(shù)字精確出現(xiàn)丟失問題

    下面小編就為大家?guī)硪黄獪\談java對象轉(zhuǎn)json, 數(shù)字精確出現(xiàn)丟失問題。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-03-03
  • jdk動態(tài)代理源碼分析過程

    jdk動態(tài)代理源碼分析過程

    這篇文章主要介紹了jkd動態(tài)代理源碼分析過程,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2019-08-08
  • 淺談Java設(shè)計(jì)模式之原型模式知識總結(jié)

    淺談Java設(shè)計(jì)模式之原型模式知識總結(jié)

    Java原型模式主要用于創(chuàng)建重復(fù)的對象,同時(shí)又能保證性能,這篇文章就帶大家仔細(xì)了解一下原型模式的知識,對正在學(xué)習(xí)java的小伙伴們很有幫助,需要的朋友可以參考下
    2021-05-05
  • 在java List中進(jìn)行模糊查詢的實(shí)現(xiàn)方法

    在java List中進(jìn)行模糊查詢的實(shí)現(xiàn)方法

    下面小編就為大家?guī)硪黄趈ava List中進(jìn)行模糊查詢的實(shí)現(xiàn)方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2016-11-11
  • Java 得到集合中所有子集

    Java 得到集合中所有子集

    本文主要介紹了Java 得到集合中所有子集的方法。具有很好的參考價(jià)值,下面跟著小編一起來看下吧
    2017-02-02
  • Java實(shí)現(xiàn)圖片上傳至FastDFS入門教程

    Java實(shí)現(xiàn)圖片上傳至FastDFS入門教程

    這篇文章主要介紹了Java實(shí)現(xiàn)圖片上傳至FastDFS入門教程,通過前端ajax提交圖片到后端,java處理服務(wù)器文件上傳至FastDFS文件服務(wù)器系統(tǒng),以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-07-07

最新評論