Java中的泛型 (进阶篇)

这一篇是关于泛型的进阶知识,如果有对泛型不了解的小伙伴可以移步第一篇基础版文章

Java中的泛型 (基础版)

 本文主要介绍类型通配符的使用、泛型的继承以及类型擦除的知识点。

目录

类型通配符

上界通配符、下界通配符

上界通配符

下界通配符

限制通配符在TreeSet中的应用

类型擦除

无限制类型擦除

添加上界的类型擦除

添加下界的类型擦除

桥方法


类型通配符

当我们想实现某个功能时,可能会出现这种情况:

这里实现一个简单的打印数字列表的方法

// 打印Integer类型的List
    public void showInteger(List<Integer> values) {
        for (Integer i : values) {
            System.out.println(i);
        }
    }

过了一会儿我又想实现一个打印Double类型列表的方法,于是:

// 打印Double类型的List
    public void showDouble(List<Double> values) {
        for (Double i : values) {
            System.out.println(i);
        }
    }

在过一会儿可能又想实现一个打印Long类型的列表,这样一来就光做了重复的工作。

这样就引入了类型通配符:

public void showNumber(List<?> values) {
        for (Object i : values) {
            System.out.println(i);
        }
    }

 在这里,使用了List<?> 当形参的类型,表示List类型是一个通配类型,实现了不管什么类型的列表,传给这个方法就可以实现打印的功能。

上界通配符、下界通配符

这一部分稍难理解

上界通配符

先说上界通配符:定义为<? extends T>, " ? " 刚才讲到了,是一个通配符,extends 也可以理解,是继承,而" T "在这里可以是泛型,也可以是具体的类型,比如Number。什么意思,通配符继承自一个类型。那么就可以理解成给这个通配符加了一个限制:只能是T类型或其子类,那不就是给通配符规定了一个上界为“ T ”吗。

如果还是不理解,那么举个例子:现在有一个盘子,如果在不加限制条件的情况下是不是盘子里什么都能装,没错,这就是不加限制的通配符“  ? ”。 那么现在我给盘子加一个限制说,这个盘子只能装水果,那么我的这个盘子里是不是就只能装苹果、香蕉等等属于水果范畴的东西,这就好加了限制条件的通配符:<? extends fruit>。

理解了什么是上界通配符后,我们来看一下怎么使用:

开头的例子中

public void showNumber(List<?> values) {
        for (Object i : values) {
            System.out.println(i);
        }
    }

我们发现一个问题,在遍历的时候居然用Object给我接收列表中的变量,我们知道,要求是打印数字列表的方法,不管是Integer、Double还是Long类型,他们都继承自Number对象,那么我们可不可以给通配符加一个限制条件<? extends Number> 用来表示我只能装来自Number对象的数据。

于是代码改为:

public  void showNumber(List<? extends Number> values) {
        for (Number i : values) {
            System.out.println(i);
        }
    }

这样的话,我们取到的就是Number的数据了,因为我知道这个List装的都是Number及其子类。

好了,这个问题解决完了,还有下一个问题,也是比较难理解的问题:

我现在想实现往列表里添加元素,于是有了:

public List<? extends Number> addList(List<? extends Number> list) {
        list.add(1);  // 编译异常
        return list;
    }

结果发现:哎呦我去,添加的时候编译异常了,要知道我这List里装的可都是Number或其子类,按理来说应该能装进去啊。

我们思考这样一个问题,之前我们说 <? extends Number> 代指的意思是限制了一个上界,所以说这个List可以是List<Integer> 或 List<Double> 或 List<Number> ,这样一来如果这是List<Double>那 是不是就不能装Integer类型的数据了,这也是上界通配符的一种特性:能用Number来读,但不能往里面写数据。后面的下界通配符正好相反:只能写,不能用Number读。真的什么都不能写了吗?其实我们可以通过反射去绕过类型检查来添加元素。

下界通配符

下界通配符,语法为 <? super T> 这里就不过多解释了,就是为通配符限制了一个下界,也就是说,通配符的类型只能是T及其父类类型。下界通配符就是用来写入数据了。

举例子:有一家宠物店有宠物笼子,猫笼子,和布偶猫笼子,现在有一只布偶猫,是不是把它放入哪个笼子都说得过去,也就是 <? super 布偶猫笼子>, 所以这样就可以写入数据了

public List<? super Number> addList(List<? super Number> list) {
        list.add(1);
        list.add(1.0);
        list.add(1.0f);
        return list;
    }

那读入数据可不可以呢,让我们实验一下:

我们用Number来接收List里的变量

public void showList(List<? super Number> list) {
        for (Number number : list) {  // 编译错误
            System.out.println(number);
        }
    }

我们发现还是报了一个编译错误,因为List里装的都是Number或其父类,无法确定就是Number类型。所以这里只能用Object类型来接收。

正确写法:

public void showList(List<? super Number> list) {
        for (Object o: list) {  // 编译错误
            System.out.println(number);
        }
    }

限制通配符在TreeSet中的应用

在TreeSet的构造方法中也使用了上下界通配符的方式

假设我们现在有三个类A、B、C,C继承B,B继承A

在TreeSet中有这样一个构造方法:

要求传入一个集合

        Set<A> setA = new HashSet<>();
        Set<B> setB = new HashSet<>();
        Set<C> setC = new HashSet<>();
        
        TreeSet<B> treeSet1 = new TreeSet<>(setA); // 报错
        TreeSet<B> treeSet2 = new TreeSet<>(setB);
        TreeSet<B> treeSet3 = new TreeSet<>(setC);

不难看出,当我们定义了B泛型的TreeSet,相应的构造方法中的泛型E就变为了B类型,然后可以传入B及其子类的Set对象进行构造

还有一个构造函数:

要求的是传入一个比较器

        Comparator<A> comparatorA = new ComparatorA();
        Comparator<B> comparatorB = new ComparatorB();
        Comparator<C> comparatorC = new ComparatorC();

        TreeSet<B> treeSet1 = new TreeSet<>(comparatorA);
        TreeSet<B> treeSet2 = new TreeSet<>(comparatorB);
        TreeSet<B> treeSet3 = new TreeSet<>(comparatorC); // 报错

这里就变为了B的子类C不能作为比较器进行传参,因为需要传B及其父类

类型擦除

基础篇中提到,泛型是JDK5后才提出的,我们知道,只要是后期出现的一种语法就会有与之前版本兼容的问题,那么泛型是如何解决兼容性问题的呢?

现在有这样一段代码:

        List<Integer> list1 = new ArrayList<>();
        List<Double> list2 = new ArrayList<>();

        System.out.println(list1.getClass() == list2.getClass());

分别给List加Integer 和 Double 泛型,然后检验他们的类是否相同

结果出人意料,返回结果是true

为什么会这样呢,这里明确定义了两种不同的泛型,为什么还会是同一个类呢。

用ASM插件观察一下编译后的字节码文件

 我们发现,我们所定义的这两个局部变量都出自一个类,而先前定义的泛型也变为了注释的形式

说明一点:JDK在进行编译的时候,把泛型类型擦除掉了。

其实我们也能想到,从一开始泛型就是为类定义了一种规范,如果不按照规范进行书写,在编译时就会编译异常。用List来解释就是:在原来的List中不加泛型的话可能什么类型的数据都能够加入List中,相当于存储Object类型了,但是如果加泛型呢,依然是存储Object类型,不过如果加入的不是我想要的数据类型就会编译异常,这样一来List中就自然存放我想要的数据类型了,而泛型在后期也会被擦除掉。

下面就深入探讨一下类型擦除的机理

无限制类型擦除

这里我定义了一个水果盘子,没有加任何限制

public class FruitPlate<T> {
    private List<T> fruits;

    public void addFruit(T fruit) { // 添加
        fruits.add(fruit);
    }

    public T getFruit(int index) {  // 获取
        return fruits.get(index);
    }

}

如果我们没有给泛型加任何限制,那么在编译之后,泛型T的类型是什么样子的呢?这里为了方便,我直接用ASM插件查看编译后的字节码文件,当然也可使用反射在运行中查看参数类型

在编译后的字节码文件中,我们发现添加方法的形参上变为了Object类型,所以说泛型在不加任何限制时被擦除会被替代为Object类型。

添加上界的类型擦除

那么我为他添加一个上界呢

假设我定义了一个水果类型

public class Fruit {
}

让水果盘子的泛型继承水果类型,就是让水果盘子只能装水果:

public class FruitPlate<T extends Fruit> {
    private List<T> fruits;

    public void addFruit(T fruit) {
        fruits.add(fruit);
    }

    public T getFruit(int index) {
        return fruits.get(index);
    }

}

再进行编译:

就发现原来的泛型被擦除成了水果类型了,原因是:我们为这个泛型规定了一个上界,所以这个上界就已经可以包含所有的类型参数了。

添加下界的类型擦除

下界就很清楚了,因为规定的是下界,一定是Object才能包含所有的类型参数。所以擦除后变为Object类型,这里不再演示了。

桥方法

定义:是 Java 泛型实现中的一种补偿机制,通过自动生成中间方法解决类型擦除后的多态冲突,确保泛型类和接口的行为符合预期

什么是多态冲突,多态我们都知道,一个接口底下可以有多个实现该接口的实现类,我们下面模拟解释什么是类型擦除后的多态冲突问题。

 定义一个泛型接口:

// 泛型接口
interface Comparable<T> {
    int compareTo(T o);
}

再定义一个实现类:

// 具体实现类(擦除后 T 变为 Object)
class MyInteger implements Comparable<Integer> {
    @Override
    public int compareTo(Integer o) {
        return 0;
    }
}

注意这里我们实现类的泛型规定为Integer类型,所以它的实现方法中就是Integer类型。我们刚才讲到类型擦除的时候提到,无限制的类型擦除都会被替换成Object类型,所以在类型擦除后原来的接口方法应该是这样的:

    public int compareTo(Object o);// 原接口方法泛型被替换为Object

根据方法重载我们知道,参数类型不同,那么方法不同,这两个方法不是同一个方法又是如何调用的呢。

为了解决这样的冲突问题,JDK则引入了桥方法的概念:

在编译时,JDK会根据继承关系、方法签名以及泛型类型擦除后的冲突情况,从而决定是否需要生成桥方法,具体的桥方法如下:

 // 编译器生成的桥方法
    public int compareTo(Object o) {
        return compareTo((Integer) o); // 类型转换并调用实际方法
    }

此方法是JDK为我们自动生成的方法,不需要自己实现。所以这个方法的执行顺序是:先检查接口方法,然后去子类中调用桥方法,最后通过桥方法调用实现方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值