java泛型 通配符详解及实践

对于泛型的原理和基础,可以参考笔者的上一篇文章
java泛型,你想知道的一切

一个问题代码

观察以下代码 :

    public static void main(String[] args) {
        // 编译报错
        // required ArrayList<Integer>, found ArrayList<Number>
        ArrayList<Integer> list1 = new ArrayList<>();
        ArrayList<Number> list2 = list1;

        // 可以正常通过编译,正常使用
        Integer[] arr1 = new Integer[]{1, 2};
        Number[] arr2 = arr1;
    }

上述代码中,在调用print函数时,产生了编译错误 required ArrayList<Integer>, found ArrayList<Number>,说需要的是ArrayList<Integer>类型,找到的却是ArrayList<Number>类型, 然后我们知道,Number类是Integer的父类,理论上向上转型,是没有问题的!

而使用java数组类型,就可以向上转型.这是为什么呢????

原因就在于, Java中泛型是不变的,而数组是协变的.

下面我们来看定义 :

不变,协变,逆变的定义

逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类);

  • f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;**
  • f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;**
  • f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系**

由此,可以对上诉代码进行解释.

数组是协变的,导致数组能够继承子元素的类型关系 : Number[] arr = new Integer[2]; -> OK

泛型是不变的,即使它的类型参数存在继承关系,但是整个泛型之间没有继承关系 : ArrayList<Number> list = new ArrayList<Integer>(); -> Error

通配符

在java泛型中,引入了 ?(通配符)符号来支持协变和逆变.

通配符表示一种未知类型,并且对这种未知类型存在约束关系.

? extends T(上边界通配符upper bounded wildcard) 对应协变关系,表示 ? 是继承自 T的任意子类型.也表示一种约束关系,只能提供数据,不能接收数据.

? 的默认实现是 ? extends Object, 表示 ? 是继承自Object的任意类型.

? super T(下边界通配符lower bounded wildcard) 对应逆变关系,表示 ?T的任意父类型.也表示一种约束关系,只能接收数据,不能提供你数据.

    public static void main(String[] args) {
        ArrayList<Integer> list1 = new ArrayList<>();
        // 协变, 可以正常转化, 表示list2是继承 Number的类型
        ArrayList<? extends Number> list2 = list1;

        // 无法正常添加
        // ? extends Number 被限制为 是继承 Number的任意类型,
        // 可能是 Integer,也可能是Float,也可能是其他继承自Number的类,
        // 所以无法将一个确定的类型添加进这个列表,除了 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. ? 表示一个未知类型, T 是表示一个确定的类型. 因此,无法使用 ?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. ? 主要针对 泛型类的限制, 无法像 T类型参数一样单独存在.如
    // OK
    static <T> void test1(T t) {
    }
    // Error
    static void test2(? t){
    }
  1. ? 表示 ? extends Object, 因此它是属于 in类型(下面会说明),无法接收数据, 而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表示声明泛型

泛型类无法使用?来声明,泛型表达式无法使用T

// Error
public class Holder<?> {
    ...
// OK
public class Holder<T> {
    ...
public static void main(String[] args) {
    // OK
    Holder<?> holder;
    // Error
    Holder<T> holder;
}
  1. 永远不要在方法返回中使用?,在方法中不会报错,但是方法的接收者将无法正常使用返回值.因为它返回了一个不确定的类型.

通配符的使用准则

学习使用泛型编程时,更令人困惑的一个方面是确定何时使用上限有界通配符以及何时使用下限有界通配符.

官方文档中提供了一些准则.

"in"类型:
“in”类型变量向代码提供数据。 如copy(src,dest) src参数提供要复制的数据,因此它是“in”类型变量的参数。

"out"类型:
“out”类型变量保存接收数据以供其他地方使用.如复制示例中,copy(src,dest),dest参数接收数据,因此它是“out”参数。

"in","out" 准则

  • "in" 类型使用 上边界通配符? extends.
  • "out" 类型使用 下边界通配符? super.
  • 如果即需要 提供数据(in), 又需要接收数据(out), 就不要使用通配符.

下面看java源码中 Collections类中的copy方法来验证该原则.

    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 接收数据, src 提供数据
                dest.set(i, src.get(i));
        }
        ...
    }

PECS(producer-extends,consumer-super)

这个是 Effective Java中提出的一种概念.

如果类型变量是 生产者,则用 extends ,如果类型变量 是消费者,则使用 super. 这种方式也成为 Get and Put Principle.
get属于生产者,put属于消费者. 这样的概念比较难懂.

继续使用上述 copy方法的例子.

// dest 消费了数据(set),则使用 super
// src 生产了数据(get), 则使用 extends
dest.set(i, src.get(i));

动手编写通配符函数

接下来我们通过通配符的知识,来模拟几个在Python语言中很常用的函数.

  1. map() 函数

在python中,map函数会根据提供的函数对指定序列做映射.

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

接下来,我们使用java泛型知识来,实现类似的功能, 方法接收一个类型的列表,可以将其转化为另一种类型的列表.

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

    /**
     * 定义一个接口,它接收一个类型,返回另一个类型.
     *
     * @param <T> 一个类型的方法参数
     * @param <R> 一个类型的返回
     */
    interface Func_TR<T, R> {
        // 接收一个类型,返回另一个类型.
        R apply(T t);
    }

    /**
     * 定义mapping函数
     *
     * @param src    提供数据,因此这里使用(get) 上边界通配符
     * @param mapper mapping 函数的具体实现
     * @param <?     extends R> 提供数据,这里是作为apply的返回值, 因此使用 上边界通配符
     * @param <?     super T>接收数据,这里作为 apply的传入参数
     * @return 返回值不要使用 通配符来定义
     */
    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 既需要接收数据(add),又需要提供数据(return),所以不使用通配符
        List<R> coll = new ArrayList<>();
        for (T t : src) {
            coll.add(mapper.apply(t));
        }
        return coll;
    }
  1. filter() 函数

Python中,filter() 函数用于过滤序列,过滤掉不符合条件的元素,返回由符合条件元素组成的新列表。

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

接下来,我们使用java泛型知识来,实现类似的功能,方法接收一个列表,和过滤方法,返回过滤后的列表.

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]
    }
    /**
     * 定义一个接口,它接收一个类型,返回布尔值
     *
     * @param <T> 一个类型的方法参数
     */
    interface Func_Tb<T> {
        boolean apply(T t);
    }

    /**
     * filter 函数的实现
     *
     * @param src  传入的列表只提供数据,这里只调用了迭代操作, 因此使用 上边界通配符
     * @param func func需要接收一个数据,  因此使用 下边界通配符
     * @return 返回值不要使用 通配符来定义,返回过滤后的列表
     */
    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 既需要接收数据(add),又需要提供数据(return),所以不使用通配符
        List<T> coll = new ArrayList<>();
        for (T t : src) {
            if (func.apply(t))
                coll.add(t);
        }
        return coll;
    }
}
  1. reduce()函数

Python中,reduce() 函数会对参数序列中元素进行累积。

函数将一个数据集合(链表,元组等)中的所有数据进行下列操作:用传给 reduce 中的函数 function(有两个参数)先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。

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

同样的, 我们利用java泛型知识,来实现类似的功能

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
    }
    /**
     * 定义一个接口,接收两个同一个类型的参数,返回值也属于同一类型
     *
     * @param <T> 作为方法参数,和返回值
     */
    interface Func_TTT<T> {
        T apply(T t1, T t2);
    }

    /**
     * reduce函数的实现
     *
     * @param src  传入的列表只提供数据,这里只调用了迭代操作, 因此使用 上边界通配符
     * @param func T 作为 apply()函数的参数和返回值,即接收也提供数据, 因此不能使用通配符
     * @return 返回值不要使用 通配符来定义, 返回参数相互迭代的值
     */
    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;
        // 将前两项的值做apply操作后的返回值,再与下一个元素进行操作
        for (int i = 1; i < size; i++) {
            T ele = src.get(i);
            result = func.apply(result, ele);
        }
        return result;
    }
}

通过这三个例子, 相信大家对java泛型以及通配符的使用,有了比较直观的了解.

参考

  1. Guidelines for Wildcard Use
  2. Java中的逆变与协变
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269