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);