java泛型 通配符詳解及實(shí)踐

對(duì)于泛型的原理和基礎(chǔ),可以參考筆者的上一篇文章
java泛型,你想知道的一切

一個(gè)問(wèn)題代碼

觀察以下代碼 :

    public static void main(String[] args) {
        // 編譯報(bào)錯(cuò)
        // required ArrayList<Integer>, found ArrayList<Number>
        ArrayList<Integer> list1 = new ArrayList<>();
        ArrayList<Number> list2 = list1;

        // 可以正常通過(guò)編譯,正常使用
        Integer[] arr1 = new Integer[]{1, 2};
        Number[] arr2 = arr1;
    }

上述代碼中,在調(diào)用print函數(shù)時(shí),產(chǎn)生了編譯錯(cuò)誤 required ArrayList<Integer>, found ArrayList<Number>,說(shuō)需要的是ArrayList<Integer>類型,找到的卻是ArrayList<Number>類型, 然后我們知道,Number類是Integer的父類,理論上向上轉(zhuǎn)型,是沒(méi)有問(wèn)題的!

而使用java數(shù)組類型,就可以向上轉(zhuǎn)型.這是為什么呢????

原因就在于, Java中泛型是不變的,而數(shù)組是協(xié)變的.

下面我們來(lái)看定義 :

不變,協(xié)變,逆變的定義

逆變與協(xié)變用來(lái)描述類型轉(zhuǎn)換(type transformation)后的繼承關(guān)系,其定義:如果A泽腮、B表示類型,f(?)表示類型轉(zhuǎn)換膛薛,≤表示繼承關(guān)系(比如米辐,A≤B表示A是由B派生出來(lái)的子類);

  • f(?)是逆變(contravariant)的,當(dāng)A≤B時(shí)有f(B)≤f(A)成立催首;**
  • f(?)是協(xié)變(covariant)的蚁趁,當(dāng)A≤B時(shí)有f(A)≤f(B)成立裙盾;**
  • f(?)是不變(invariant)的,當(dāng)A≤B時(shí)上述兩個(gè)式子均不成立他嫡,即f(A)與f(B)相互之間沒(méi)有繼承關(guān)系**

由此,可以對(duì)上訴代碼進(jìn)行解釋.

數(shù)組是協(xié)變的,導(dǎo)致數(shù)組能夠繼承子元素的類型關(guān)系 : Number[] arr = new Integer[2]; -> OK

泛型是不變的,即使它的類型參數(shù)存在繼承關(guān)系,但是整個(gè)泛型之間沒(méi)有繼承關(guān)系 : ArrayList<Number> list = new ArrayList<Integer>(); -> Error

通配符

在java泛型中,引入了 ?(通配符)符號(hào)來(lái)支持協(xié)變和逆變.

通配符表示一種未知類型,并且對(duì)這種未知類型存在約束關(guān)系.

? extends T(上邊界通配符upper bounded wildcard) 對(duì)應(yīng)協(xié)變關(guān)系,表示 ? 是繼承自 T的任意子類型.也表示一種約束關(guān)系,只能提供數(shù)據(jù),不能接收數(shù)據(jù).

? 的默認(rèn)實(shí)現(xiàn)是 ? extends Object, 表示 ? 是繼承自Object的任意類型.

? super T(下邊界通配符lower bounded wildcard) 對(duì)應(yīng)逆變關(guān)系,表示 ?T的任意父類型.也表示一種約束關(guān)系,只能接收數(shù)據(jù),不能提供你數(shù)據(jù).

    public static void main(String[] args) {
        ArrayList<Integer> list1 = new ArrayList<>();
        // 協(xié)變, 可以正常轉(zhuǎn)化, 表示list2是繼承 Number的類型
        ArrayList<? extends Number> list2 = list1;

        // 無(wú)法正常添加
        // ? extends Number 被限制為 是繼承 Number的任意類型,
        // 可能是 Integer,也可能是Float,也可能是其他繼承自Number的類,
        // 所以無(wú)法將一個(gè)確定的類型添加進(jìn)這個(gè)列表,除了 null之外
        list2.add(new Integer(1));
        // 可以添加
        list2.add(null);

        // 逆變
        ArrayList<Number> list3 = new ArrayList<>();
        ArrayList<? super Number> list4 = list3;
        list4.add(new Integer(1));
    }

? 與 T 的差別

  1. ? 表示一個(gè)未知類型, T 是表示一個(gè)確定的類型. 因此,無(wú)法使用 ?T 聲明變量和使用變量.如
    // OK
    static <T> void test1(List<T> list) {
        T t = list.get(0);
        t.toString();
    }
    // Error
    static void test2(List<?> list){
        ? t = list.get(0);
        t.toString();
    }```java
  1. ? 主要針對(duì) 泛型類的限制, 無(wú)法像 T類型參數(shù)一樣單獨(dú)存在.如
    // OK
    static <T> void test1(T t) {
    }
    // Error
    static void test2(? t){
    }
  1. ? 表示 ? extends Object, 因此它是屬于 in類型(下面會(huì)說(shuō)明),無(wú)法接收數(shù)據(jù), 而T可以.
    // OK
    static <T> void test1(List<T> list, T t) {
        list.add(t);
    }
    // Error
    static void test2(List<?> list, Object t) {
        list.add(t);
    }
  1. ? 主要表示使用泛型,T表示聲明泛型

泛型類無(wú)法使用?來(lái)聲明,泛型表達(dá)式無(wú)法使用T

// Error
public class Holder<?> {
    ...
// OK
public class Holder<T> {
    ...
public static void main(String[] args) {
    // OK
    Holder<?> holder;
    // Error
    Holder<T> holder;
}
  1. 永遠(yuǎn)不要在方法返回中使用?,在方法中不會(huì)報(bào)錯(cuò),但是方法的接收者將無(wú)法正常使用返回值.因?yàn)樗祷亓艘粋€(gè)不確定的類型.

通配符的使用準(zhǔn)則

學(xué)習(xí)使用泛型編程時(shí)番官,更令人困惑的一個(gè)方面是確定何時(shí)使用上限有界通配符以及何時(shí)使用下限有界通配符.

官方文檔中提供了一些準(zhǔn)則.

"in"類型:
“in”類型變量向代碼提供數(shù)據(jù)。 如copy(src钢属,dest) src參數(shù)提供要復(fù)制的數(shù)據(jù)徘熔,因此它是“in”類型變量的參數(shù)。

"out"類型:
“out”類型變量保存接收數(shù)據(jù)以供其他地方使用.如復(fù)制示例中淆党,copy(src酷师,dest),dest參數(shù)接收數(shù)據(jù)染乌,因此它是“out”參數(shù)山孔。

"in","out" 準(zhǔn)則

  • "in" 類型使用 上邊界通配符? extends.
  • "out" 類型使用 下邊界通配符? super.
  • 如果即需要 提供數(shù)據(jù)(in), 又需要接收數(shù)據(jù)(out), 就不要使用通配符.

下面看java源碼中 Collections類中的copy方法來(lái)驗(yàn)證該原則.

    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        ...
        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                // dest 接收數(shù)據(jù), src 提供數(shù)據(jù)
                dest.set(i, src.get(i));
        }
        ...
    }

PECS(producer-extends,consumer-super)

這個(gè)是 Effective Java中提出的一種概念.

如果類型變量是 生產(chǎn)者,則用 extends ,如果類型變量 是消費(fèi)者,則使用 super. 這種方式也成為 Get and Put Principle.
get屬于生產(chǎn)者,put屬于消費(fèi)者. 這樣的概念比較難懂.

繼續(xù)使用上述 copy方法的例子.

// dest 消費(fèi)了數(shù)據(jù)(set),則使用 super
// src 生產(chǎn)了數(shù)據(jù)(get), 則使用 extends
dest.set(i, src.get(i));

動(dòng)手編寫(xiě)通配符函數(shù)

接下來(lái)我們通過(guò)通配符的知識(shí),來(lái)模擬幾個(gè)在Python語(yǔ)言中很常用的函數(shù).

  1. map() 函數(shù)

在python中,map函數(shù)會(huì)根據(jù)提供的函數(shù)對(duì)指定序列做映射.

strArr = ["1", "2"]
intArr = map(lambda x: int(x) * 10, strArr)
print(strArr,list(intArr))
# ['1', '2'] [10, 20]

接下來(lái),我們使用java泛型知識(shí)來(lái),實(shí)現(xiàn)類似的功能, 方法接收一個(gè)類型的列表,可以將其轉(zhuǎn)化為另一種類型的列表.

public class Main {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        strList.add("1");
        strList.add("2");
        // jdk8 使用lambda表達(dá)式
        List<Integer> intList = map(strList, s -> Integer.parseInt(s) * 10);
        // strList["1","2"]
        // intList[10,20]
    }

    /**
     * 定義一個(gè)接口,它接收一個(gè)類型,返回另一個(gè)類型.
     *
     * @param <T> 一個(gè)類型的方法參數(shù)
     * @param <R> 一個(gè)類型的返回
     */
    interface Func_TR<T, R> {
        // 接收一個(gè)類型,返回另一個(gè)類型.
        R apply(T t);
    }

    /**
     * 定義mapping函數(shù)
     *
     * @param src    提供數(shù)據(jù),因此這里使用(get) 上邊界通配符
     * @param mapper mapping 函數(shù)的具體實(shí)現(xiàn)
     * @param <?     extends R> 提供數(shù)據(jù),這里是作為apply的返回值, 因此使用 上邊界通配符
     * @param <?     super T>接收數(shù)據(jù),這里作為 apply的傳入?yún)?shù)
     * @return 返回值不要使用 通配符來(lái)定義
     */
    public static <R, T> List<R> map(List<? extends T> src, Func_TR<? super T, ? extends R> mapper) {
        if (src == null)
            throw new IllegalArgumentException("List must not be not null");
        if (mapper == null)
            throw new IllegalArgumentException("map func must be not null");
        // coll 既需要接收數(shù)據(jù)(add),又需要提供數(shù)據(jù)(return),所以不使用通配符
        List<R> coll = new ArrayList<>();
        for (T t : src) {
            coll.add(mapper.apply(t));
        }
        return coll;
    }
  1. filter() 函數(shù)

Python中,filter() 函數(shù)用于過(guò)濾序列,過(guò)濾掉不符合條件的元素荷憋,返回由符合條件元素組成的新列表台颠。

intArr = [1, 2, 3, 4, 5]
newArr = filter(lambda x: x >= 3, intArr)
print(list(newArr))
# [1, 2, 3, 4, 5] [3, 4, 5]

接下來(lái),我們使用java泛型知識(shí)來(lái),實(shí)現(xiàn)類似的功能,方法接收一個(gè)列表,和過(guò)濾方法,返回過(guò)濾后的列表.

public class Main {
    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        intList.add(1);
        intList.add(2);
        intList.add(3);
        intList.add(4);
        intList.add(5);
        
        List<Integer> filterList = filter(intList, i -> i >= 3);
        // filterList[3,4,5]
    }
    /**
     * 定義一個(gè)接口,它接收一個(gè)類型,返回布爾值
     *
     * @param <T> 一個(gè)類型的方法參數(shù)
     */
    interface Func_Tb<T> {
        boolean apply(T t);
    }

    /**
     * filter 函數(shù)的實(shí)現(xiàn)
     *
     * @param src  傳入的列表只提供數(shù)據(jù),這里只調(diào)用了迭代操作, 因此使用 上邊界通配符
     * @param func func需要接收一個(gè)數(shù)據(jù),  因此使用 下邊界通配符
     * @return 返回值不要使用 通配符來(lái)定義,返回過(guò)濾后的列表
     */
    public static <T> List<T> filter(List<? extends T> src, Func_Tb<? super T> func) {
        if (src == null)
            throw new IllegalArgumentException("List must not be not null");
        if (func == null)
            throw new IllegalArgumentException("filter func must be not null");

        // coll 既需要接收數(shù)據(jù)(add),又需要提供數(shù)據(jù)(return),所以不使用通配符
        List<T> coll = new ArrayList<>();
        for (T t : src) {
            if (func.apply(t))
                coll.add(t);
        }
        return coll;
    }
}
  1. reduce()函數(shù)

Python中,reduce() 函數(shù)會(huì)對(duì)參數(shù)序列中元素進(jìn)行累積。

函數(shù)將一個(gè)數(shù)據(jù)集合(鏈表台谊,元組等)中的所有數(shù)據(jù)進(jìn)行下列操作:用傳給 reduce 中的函數(shù) function(有兩個(gè)參數(shù))先對(duì)集合中的第 1蓉媳、2 個(gè)元素進(jìn)行操作譬挚,得到的結(jié)果再與第三個(gè)數(shù)據(jù)用 function 函數(shù)運(yùn)算,最后得到一個(gè)結(jié)果酪呻。

from functools import reduce
result = reduce(lambda x, y: x + y, [1, 2, 3, 4, 5])
print(result)
# 15

同樣的, 我們利用java泛型知識(shí),來(lái)實(shí)現(xiàn)類似的功能

public class Main {
    public static void main(String[] args) {
        List<Integer> intList = new ArrayList<>();
        intList.add(1);
        intList.add(2);
        intList.add(3);
        intList.add(4);
        intList.add(5);

        int result = reduce(intList, (t1, t2) -> t1 + t2);
        // result = 15
    }
    /**
     * 定義一個(gè)接口,接收兩個(gè)同一個(gè)類型的參數(shù),返回值也屬于同一類型
     *
     * @param <T> 作為方法參數(shù),和返回值
     */
    interface Func_TTT<T> {
        T apply(T t1, T t2);
    }

    /**
     * reduce函數(shù)的實(shí)現(xiàn)
     *
     * @param src  傳入的列表只提供數(shù)據(jù),這里只調(diào)用了迭代操作, 因此使用 上邊界通配符
     * @param func T 作為 apply()函數(shù)的參數(shù)和返回值,即接收也提供數(shù)據(jù), 因此不能使用通配符
     * @return 返回值不要使用 通配符來(lái)定義, 返回參數(shù)相互迭代的值
     */
    public static <T> T reduce(List<? extends T> src, Func_TTT<T> func) {
        if (src == null || src.size() == 0)
            throw new IllegalArgumentException("List must not be not null or empty");
        if (func == null)
            throw new IllegalArgumentException("reduce func must be not null");

        int size   = src.size();
        T   result = src.get(0);
        if (size == 1) return result;
        // 將前兩項(xiàng)的值做apply操作后的返回值,再與下一個(gè)元素進(jìn)行操作
        for (int i = 1; i < size; i++) {
            T ele = src.get(i);
            result = func.apply(result, ele);
        }
        return result;
    }
}

通過(guò)這三個(gè)例子, 相信大家對(duì)java泛型以及通配符的使用,有了比較直觀的了解.

參考

  1. Guidelines for Wildcard Use
  2. Java中的逆變與協(xié)變
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末减宣,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子玩荠,更是在濱河造成了極大的恐慌漆腌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,376評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阶冈,死亡現(xiàn)場(chǎng)離奇詭異闷尿,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)女坑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén)填具,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人匆骗,你說(shuō)我怎么就攤上這事劳景。” “怎么了碉就?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,966評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵盟广,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我瓮钥,道長(zhǎng)筋量,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,432評(píng)論 1 283
  • 正文 為了忘掉前任碉熄,我火速辦了婚禮桨武,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘具被。我一直安慰自己玻募,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,519評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布一姿。 她就那樣靜靜地躺著七咧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪叮叹。 梳的紋絲不亂的頭發(fā)上艾栋,一...
    開(kāi)封第一講書(shū)人閱讀 49,792評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音蛉顽,去河邊找鬼蝗砾。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的悼粮。 我是一名探鬼主播闲勺,決...
    沈念sama閱讀 38,933評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼扣猫!你這毒婦竟也來(lái)了菜循?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,701評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤申尤,失蹤者是張志新(化名)和其女友劉穎癌幕,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體昧穿,經(jīng)...
    沈念sama閱讀 44,143評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡勺远,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,488評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了时鸵。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胶逢。...
    茶點(diǎn)故事閱讀 38,626評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖寥枝,靈堂內(nèi)的尸體忽然破棺而出宪塔,到底是詐尸還是另有隱情,我是刑警寧澤囊拜,帶...
    沈念sama閱讀 34,292評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站比搭,受9級(jí)特大地震影響冠跷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜身诺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,896評(píng)論 3 313
  • 文/蒙蒙 一蜜托、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧霉赡,春花似錦橄务、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至嗓化,卻和暖如春棠涮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背刺覆。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工严肪, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓驳糯,卻偏偏與公主長(zhǎng)得像篇梭,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子酝枢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,494評(píng)論 2 348