java泛型(协变,逆变以及坑)

1:协变(extends:PECS(Producer-Extends, Consumer-Super))

1.1:先看看直接使用两种类型的泛型

	/**
     * 不同的泛型
     */
    @Test
    public void differentGeneric() {
        // 编译失败
        List<Number> list = new ArrayList<Integer>();
    }

编译失败

在这里插入图片描述

1.2:改成使用协变

	/**
     * 协变1
     */
    @Test
    public void covariant1() {
        // 编译通过
        List<? extends Number> list = new ArrayList<Integer>();

        // 编译失败
        list.add(1);

        // 编译失败
        list.add((Number) 1);

        // 编译通过
        Number number = list.get(0);
    }

编译失败,List创建成功了,不能使用add添加,但是可以使用get方法获取

在这里插入图片描述

1.3:不能add,如何使用协变,直接使用赋值或者当方法

	/**
     * 协变2
     */
    @Test
    public void covariant2() {
        List<Integer> list = new ArrayList<>();

        List<? extends Number> list1 = list;
    }

    /**
     * 协变3
     */
    public List<? extends Number> covariant3() {
        return new ArrayList<>();
    }

1.4:数组的协变

	/**
     * 数组协变
     */
    @Test
    public void arrayCovariant() {
        // 编译通过
        Number[] array = new Integer[10];

        // 编译通过
        array[0] = 1;

        // 编译通过
        array[1] = 1.0;
    }

编译成功,但是存入浮点型数据会报错

在这里插入图片描述

2:逆变(super:PECS(Producer-Extends, Consumer-Super))

	/**
     * 逆变
     */
    @Test
    public void contravariant() {
        // 编译通过
        List<? super Integer> list = new ArrayList<Number>();

        // 编译通过
        list.add(1);

        // 编译失败
        Number obj1 = list.get(0);

        // 编译失败
        Integer obj2 = list.get(0);

        // 编译通过
        Object obj3 = list.get(0);
    }

编译失败,可以add,但是不能按泛型类型获取,直接get,得到的是Object类型

在这里插入图片描述

3:new泛型

	 /**
     * new泛型
     *
     */
    @Test
    public void newGeneric() {
        class Test<T extends String> {
            public Test(T param) throws Exception {
                // 编译失败
                T t = new T();

                // 编译通过
                Object obj = param.getClass().newInstance();
            }
        }
    }

编译失败,不能直接new创建对象,但是可以通过反射的方式创建

在这里插入图片描述

4:? 泛型:等同于 <? extends Object>

	/**
     * ? 泛型
     */
    @Test
    public void anyGeneric() {
        List<? extends Object> list = Arrays.asList("1", 2);
        List<?> list1 = list;
    }	

5:泛型的坑

使用BeanUtils进行对象的拷贝

static class OrderBo {
        private List<String> list;

        OrderBo(List<String> list) {
            this.list = list;
        }

        public List<String> getList() {
            return list;
        }

        public void setList(List<String> list) {
            this.list = list;
        }
    }

    static class OrderDto {
        private List<Integer> list;

        OrderDto(List<Integer> list) {
            this.list = list;
        }

        public List<Integer> getList() {
            return list;
        }

        public void setList(List<Integer> list) {
            this.list = list;
        }
    }

    @Test
    public void bo2Dto() {
        OrderBo orderBo = new OrderBo(Arrays.asList("hello", "world"));
        OrderDto orderDto = new OrderDto(Collections.emptyList());

        BeanUtils.copyProperties(orderBo, orderDto);

        // 打印list
        System.out.println(orderDto.getList());

        // 打印list第一个元素
        System.out.println(orderDto.getList().get(0));

        // list第一个元素,强转为Object,然后调用toString,最后再打印
        System.out.println(((Object) orderDto.getList().get(0)).toString());

        // list第一个元素调用toString,然后再打印
        System.out.println(orderDto.getList().get(0).toString());
    }

输出:可以发现发现数据拷贝成功了,把Stirng类型的数据拷贝到了Integer类型的集合中,并且可以读取到整个list,从list中取出对象也是成功,但是到最后调用取出对象的 toString 方法就出现了报错

在这里插入图片描述

修改一下拷贝对象

	@Test
    public void dto2Bo() {
        OrderBo orderBo = new OrderBo(Collections.emptyList());
        OrderDto orderDto = new OrderDto(Arrays.asList(1, 2));

        BeanUtils.copyProperties(orderDto, orderBo);

        // 打印list
        System.out.println(orderBo.getList());

        // 打印list第一个元素
        System.out.println(orderBo.getList().get(0));
    }

输出:拷贝还是成功,获取list也成功,但是这个时候获取集合第一个对象进行打印的时候却出现异常

在这里插入图片描述

结果分析

1:首先查看一下第一个拷贝,方法:bo2Dto 的字节码,从64(invokevirtual 指令)这里可以发现打印 get(0) 调用的是入参类型为 Object 的 println 方法,接着看64后面并没有出现 checkcast ,直到代码最后一行打印 orderDto.getList().get(0).toString() 却出现了改指令,校验转换的类型,在这里校验失败抛出异常

在这里插入图片描述
2:接着查看第二个拷贝,方法:dto2Bo 的字节码,从 LineNumberTable 定位到最后一行打印的代码,对应的序号是55,从55往下看,发现真正打印是序号71这个位置,可以看到调用的是入参类型为 String 的 println 方法(上面的是 Object ),这里执行的方法就和上面不同了。并且在打印之前68这个位置出现了checkcast 指令检查转换类型,在这里校验失败抛出异常

在这里插入图片描述
3:从上面的字节码分析,可以发现 println 调用的是两个不同的方法,并且第二个拷贝在打印 get(0) 就出现了类型转化,执行 checkcast 指令,而第一个拷贝方法是在调用 toString 方法才进行了类型检查。

第一个方法在打印 get(0) 的时候,因为 println 方法进行了重载(一共10个方法),这里时候需要确定具体执行哪个方法(编译时已经确定),因为泛型为 Integer ,所以对应就执行到入参为 Object 的 println 方法(这里比较有趣的是没有匹配到入参为 int 的 println 方法,因为 Integer 是包装类,优先匹配的是 Object )。但是泛型在编译之后都会被擦除成 Object 类型(看字节码get返回的都是Object类型),已经被擦除为 Object 所以调用 println 方法的时候就不用进行类型转换。

但是到第二方法的时候,因为泛型为 String ,所以对应就执行到入参为 String 的 println 方法,这个时候就需要将 Object 转为 String 作为入参传递 println 中,也就是这个时候触发了 checkcast 指令。

回到第一个方法中,方法是在调用 orderDto.getList().get(0).toString() 出现了 checkcast 指令,这个就说明了泛型对象在调用具体的方法前会做类型转换。但是如果去看打印 get(0) 的时候,调用入参为 Object 的 println 方法,该方法会调用 String.valueOf ,接着会调用入参为 Object 的 toString 方法。既然 orderDto.getList().get(0) 的结果会泛型擦除为 Object ,那么就相当于都是 Object 类型调用toString 方法,为什么一个进行类型转换,另外一个却没有进行类型转换。

orderDto.getList().get(0).toString() 出现了类型转换,这是因为调用具体的方法,在编译的时候需要确定泛型能不能执行该方法,比如泛型对应的类型根本没有该方法,那么编译就会失败。 get(0) 则是作为 println 的 Object 类型入参,入参已经匹配说明类型已经确定没有问题,后续方法深处调用 toString 方法就没有必要类型转化,动态分派自然就会执行到子类的重写方法中。

代码中也可以看到打印 ((Object) orderDto.getList().get(0)).toString() 的时候并没有报错,并且字节码中也没有触发了 checkcast 指令。类似的,如果有 Object obj = orderDto.getList().get(0) 这样一个赋值操作,在字节码中也是不会出现 checkcast 指令。

4:上面说到泛型擦除为 Object,但是实际的类型,在运行时候还是可以知道,对象头,局部变量类型表等都有记录

	@Test
    public void bo2Dto2() {
        OrderBo orderBo = new OrderBo(Arrays.asList("hello", "world"));
        OrderDto orderDto = new OrderDto(Collections.emptyList());

        BeanUtils.copyProperties(orderBo, orderDto);

        // 打印Object类型的对象头
        System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());

        // 打印String类型的对象头
        System.out.println(ClassLayout.parseInstance(new String()).toPrintable());

        // 打印list第一个元素的对象头
        System.out.println(ClassLayout.parseInstance(orderDto.getList().get(0)).toPrintable());
    }

输出:可以看到对象头的具体信息

在这里插入图片描述

5:什么时候泛型会进行类型转换,总结来说

1:泛型对象调用具体的方法或变量
2:泛型对象作为方法入参,如果入参不是Object类型,则需要向下转型
3:泛型对象直接向下转型,如:((Integer) orderDto.getList().get(0));
4:泛型对象赋值给具体类型(除:Object)的变量,如:Integer integer = orderDto.getList().get(0);

6:参考视频

一个非常隐蔽的BUG!泛型的正确使用方式!高级面试高频题!
90%的人都不懂的泛型,泛型的缺陷和应用场景

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值