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中的逆变与协变
目录
相关文章
|
3天前
|
存储 安全 Java
系统安全架构的深度解析与实践:Java代码实现
【11月更文挑战第1天】系统安全架构是保护信息系统免受各种威胁和攻击的关键。作为系统架构师,设计一套完善的系统安全架构不仅需要对各种安全威胁有深入理解,还需要熟练掌握各种安全技术和工具。
27 10
|
1天前
|
缓存 Java 调度
Java中的多线程编程:从基础到实践
【10月更文挑战第24天】 本文旨在为读者提供一个关于Java多线程编程的全面指南。我们将从多线程的基本概念开始,逐步深入到Java中实现多线程的方法,包括继承Thread类、实现Runnable接口以及使用Executor框架。此外,我们还将探讨多线程编程中的常见问题和最佳实践,帮助读者在实际项目中更好地应用多线程技术。
9 3
|
3天前
|
监控 安全 Java
Java多线程编程的艺术与实践
【10月更文挑战第22天】 在现代软件开发中,多线程编程是一项不可或缺的技能。本文将深入探讨Java多线程编程的核心概念、常见问题以及最佳实践,帮助开发者掌握这一强大的工具。我们将从基础概念入手,逐步深入到高级主题,包括线程的创建与管理、同步机制、线程池的使用等。通过实际案例分析,本文旨在提供一种系统化的学习方法,使读者能够在实际项目中灵活运用多线程技术。
|
1天前
|
缓存 安全 Java
Java中的多线程编程:从基础到实践
【10月更文挑战第24天】 本文将深入探讨Java中的多线程编程,包括其基本原理、实现方式以及常见问题。我们将从简单的线程创建开始,逐步深入了解线程的生命周期、同步机制、并发工具类等高级主题。通过实际案例和代码示例,帮助读者掌握多线程编程的核心概念和技术,提高程序的性能和可靠性。
7 2
|
2天前
|
Java
Java中的多线程编程:从基础到实践
本文深入探讨Java多线程编程,首先介绍多线程的基本概念和重要性,接着详细讲解如何在Java中创建和管理线程,最后通过实例演示多线程的实际应用。文章旨在帮助读者理解多线程的核心原理,掌握基本的多线程操作,并能够在实际项目中灵活运用多线程技术。
|
7天前
|
Java API 调度
Java中的多线程编程:理解与实践
本文旨在为读者提供对Java多线程编程的深入理解,包括其基本概念、实现方式以及常见问题的解决方案。通过阅读本文,读者将能够掌握Java多线程编程的核心知识,提高自己在并发编程方面的技能。
|
5天前
|
消息中间件 监控 算法
Java性能优化:策略与实践
【10月更文挑战第21】Java性能优化:策略与实践
|
5天前
|
Java API
[Java]泛型
本文详细介绍了Java泛型的相关概念和使用方法,包括类型判断、继承泛型类或实现泛型接口、泛型通配符、泛型方法、泛型上下边界、静态方法中使用泛型等内容。作者通过多个示例和测试代码,深入浅出地解释了泛型的原理和应用场景,帮助读者更好地理解和掌握Java泛型的使用技巧。文章还探讨了一些常见的疑惑和误区,如泛型擦除和基本数据类型数组的使用限制。最后,作者强调了泛型在实际开发中的重要性和应用价值。
[Java]泛型
|
6天前
|
Java 开发者
Java中的异常处理:理解与实践
【10月更文挑战第20天】在Java编程的世界中,异常处理是一个不可忽视的重要话题。它不仅关乎代码的健壮性,更是程序设计中体现开发者预见性和解决问题能力的关键所在。本文将通过深入浅出的方式,带领读者探索Java异常处理的核心概念、实践方法以及如何优雅地应对各种异常情况,让你的代码在面对意外时也能从容不迫。
7 1
|
13天前
|
安全 Java UED
Java中的多线程编程:从基础到实践
本文深入探讨了Java中的多线程编程,包括线程的创建、生命周期管理以及同步机制。通过实例展示了如何使用Thread类和Runnable接口来创建线程,讨论了线程安全问题及解决策略,如使用synchronized关键字和ReentrantLock类。文章还涵盖了线程间通信的方式,包括wait()、notify()和notifyAll()方法,以及如何避免死锁。此外,还介绍了高级并发工具如CountDownLatch和CyclicBarrier的使用方法。通过综合运用这些技术,可以有效提高多线程程序的性能和可靠性。