java泛型 通配符详解及实践

简介: 对于泛型的原理和基础,可以参考笔者的上一篇文章java泛型,你想知道的一切一个问题代码观察以下代码 : public static void main(String[] args) { // 编译报错 // ...

对于泛型的原理和基础,可以参考笔者的上一篇文章
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中的逆变与协变
目录
相关文章
|
7月前
|
监控 Java API
现代 Java IO 高性能实践从原理到落地的高效实现路径与实战指南
本文深入解析现代Java高性能IO实践,涵盖异步非阻塞IO、操作系统优化、大文件处理、响应式网络编程与数据库访问,结合Netty、Reactor等技术落地高并发应用,助力构建高效可扩展的IO系统。
215 0
|
8月前
|
资源调度 安全 Java
Java 大数据在智能教育在线实验室设备管理与实验资源优化配置中的应用实践
本文探讨Java大数据技术在智能教育在线实验室设备管理与资源优化中的应用。通过统一接入异构设备、构建四层实时处理管道及安全防护双体系,显著提升设备利用率与实验效率。某“双一流”高校实践显示,设备利用率从41%升至89%,等待时间缩短78%。该方案降低管理成本,为教育数字化转型提供技术支持。
235 1
|
5月前
|
安全 Java
Java之泛型使用教程
Java之泛型使用教程
401 10
|
7月前
|
SQL 缓存 安全
深度理解 Java 内存模型:从并发基石到实践应用
本文深入解析 Java 内存模型(JMM),涵盖其在并发编程中的核心作用与实践应用。内容包括 JMM 解决的可见性、原子性和有序性问题,线程与内存的交互机制,volatile、synchronized 和 happens-before 等关键机制的使用,以及在单例模式、线程通信等场景中的实战案例。同时,还介绍了常见并发 Bug 的排查与解决方案,帮助开发者写出高效、线程安全的 Java 程序。
412 0
|
7月前
|
并行计算 Java API
Java List 集合结合 Java 17 新特性与现代开发实践的深度解析及实战指南 Java List 集合
本文深入解析Java 17中List集合的现代用法,结合函数式编程、Stream API、密封类、模式匹配等新特性,通过实操案例讲解数据处理、并行计算、响应式编程等场景下的高级应用,帮助开发者提升集合操作效率与代码质量。
313 1
|
7月前
|
安全 Java API
Java 17 及以上版本核心特性在现代开发实践中的深度应用与高效实践方法 Java 开发实践
本项目以“学生成绩管理系统”为例,深入实践Java 17+核心特性与现代开发技术。采用Spring Boot 3.1、WebFlux、R2DBC等构建响应式应用,结合Record类、模式匹配、Stream优化等新特性提升代码质量。涵盖容器化部署(Docker)、自动化测试、性能优化及安全加固,全面展示Java最新技术在实际项目中的应用,助力开发者掌握现代化Java开发方法。
328 1
|
7月前
|
安全 Java API
在Java中识别泛型信息
以上步骤和示例代码展示了怎样在Java中获取泛型类、泛型方法和泛型字段的类型参数信息。这些方法利用Java的反射API来绕过类型擦除的限制并访问运行时的类型信息。这对于在运行时进行类型安全的操作是很有帮助的,比如在创建类型安全的集合或者其他复杂数据结构时处理泛型。注意,过度使用反射可能会导致代码难以理解和维护,因此应该在确有必要时才使用反射来获取泛型信息。
266 11
|
7月前
|
存储 搜索推荐 算法
Java 大视界 -- Java 大数据在智慧文旅旅游线路规划与游客流量均衡调控中的应用实践(196)
本实践案例深入探讨了Java大数据技术在智慧文旅中的创新应用,聚焦旅游线路规划与游客流量调控难题。通过整合多源数据、构建用户画像、开发个性化推荐算法及流量预测模型,实现了旅游线路的精准推荐与流量的科学调控。在某旅游城市的落地实践中,游客满意度显著提升,景区流量分布更加均衡,充分展现了Java大数据技术在推动文旅产业智能化升级中的核心价值与广阔前景。
|
8月前
|
数据采集 机器学习/深度学习 Java
Java 大视界 —— Java 大数据在智慧交通停车场智能管理与车位预测中的应用实践(174)
本文围绕 Java 大数据在智慧交通停车场智能管理与车位预测中的应用展开,深入剖析行业痛点,系统阐述大数据技术的应用架构,结合大型体育中心停车场案例,展示系统实施过程与显著成效,提供极具实操价值的技术方案。
|
8月前
|
Java 数据库连接 API
Java 对象模型现代化实践 基于 Spring Boot 与 MyBatis Plus 的实现方案深度解析
本文介绍了基于Spring Boot与MyBatis-Plus的Java对象模型现代化实践方案。采用Spring Boot 3.1.2作为基础框架,结合MyBatis-Plus 3.5.3.1进行数据访问层实现,使用Lombok简化PO对象,MapStruct处理对象转换。文章详细讲解了数据库设计、PO对象实现、DAO层构建、业务逻辑封装以及DTO/VO转换等核心环节,提供了一个完整的现代化Java对象模型实现案例。通过分层设计和对象转换,实现了业务逻辑与数据访问的解耦,提高了代码的可维护性和扩展性。
324 1