Java协变、逆变、类型擦除
协变、逆变
定义
Java中String类型是继承自Object的,姑且记做String ≦ Object,表示String是Object的子类型,String的对象可以赋给Object的对象。而Object的数组类型Object[],理解成是由Object构造出来的一种新的类型,可以认为是一种构造类型,记f(Object),那么可以这么来描述协变和逆变:
当A ≦ B时,如果有f(A) ≦ f(B),那么f叫做协变(子类赋值给父类);
当A ≦ B时,如果有f(B) ≦ f(A),那么f叫做逆变(父类赋值给子类);
如果上面两种关系都不成立则叫做不可变。
数组协变
代码
@Test
public void testZero(){
Food food = new Fruit();
// or
food = new Meat(); // 即 把子类赋值给父类引用
Fruit [] arrFruit = new Fruit[3];
Food [] arrFood = new Food[3];
arrFood=arrFruit; // 数组协变,把子类数组赋值给父类数组
//arrFruit=arrFood;//error 不能逆变
}
泛型协变与逆变
泛型
泛型没有内建的协变类型
代码
@Test
public void testOne(){
List<Meat> beefListRoot=new ArrayList<>();
// List<Food> foodList=beefListRoot; //错误:不可协变,即子类list不能赋值给父类list
List<Food> foodListRoot=new ArrayList<>();
// beefListRoot=foodListRoot; //错误 : 不可逆变,即父类list不能赋值给子类list
}
我们可以使用通配符实现泛型的协变和逆变
通配符协变
代码
@Test
public void testTwo(){
List<? extends Food> foodList = new ArrayList<>();
List<Apple> appleList = new ArrayList<>();
foodList = appleList; // ok 协变,即子类list赋值给父类list
// foodList.add(new Apple());//不能执行添加null 以外的操作,原因:反正法:beef也是food子类,但是不该加入苹果列表,否则get时类型转换异常,就有问题
Food food = foodList.get(0); //ok, 把子类引用赋值给父类显然是可以的
}
通配符逆变
代码
@Test
public void testThree(){
List<? super Fruit> fruitList = new ArrayList<>();
List<Food> foodList = new ArrayList<>();
foodList.add(new Meat());
fruitList = foodList; // ok 逆变,父类列表赋值给子类列表
fruitList.add(new Apple()); // ok,只能添加 Fruit 或者 其子类
// fruitList.add(new Food());// error, 只能添加 Fruit 或者 其子类
//Fruit fruit = fruitList.get(0); // error,get出来的元素是Object类型
Object obj = fruitList.get(0);// ok
}
通配符的协变和逆变使用场景
如果参数化类型表示一个生产者,就使用<? extends T>。比如list.get(0)这种,list作为数据源producer;
如果它表示一个消费者,就使用<? super T>。比如:list.add(new Apple()),list作为数据处理端consumer。
类型擦除
定义
Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。
如在代码中定义List和List等类型,在编译后都会变成List,JVM看到的只是List,而由泛型附加的类型信息对JVM是看不到的。
使用泛型获取返回值之前,泛型变量进行强转。
如:
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
相关定义
- 原始类型
就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。
如:
ArrayList<String>
原始类型为Object;ArrayList<T extend Apple>
原始类型为Apple;ArrayList<T super Apple>
原始类型为Object;
证明泛型擦除的案例
- 1
@Test
public void test() {
List<String> ls=new ArrayList<String>();
List<Integer> ln=new ArrayList<Integer>();
//class java.util.ArrayList
System.out.println(ls.getClass());
//class java.util.ArrayList
System.out.println(ln.getClass());
}
- 2
@Test
public void testTwo() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
ArrayList<Integer> list=new ArrayList<Integer>();
//获取到list对象的add方法
Method testTwo = list.getClass().getMethod("add",Object.class);
//添加数据,定义泛型为整形,但是反射获取类型后可以进行添加String
testTwo.invoke(list, "wqewqe");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
Gitte代码
泛型擦除:https://gitee.com/zhuayng/foundation-study/blob/develop/JavaBasis/Other/src/main/java/com/yxkj/other/modular/wildcard/erase/GenericErase.java
逆变与协变:https://gitee.com/zhuayng/foundation-study/blob/develop/JavaBasis/Other/src/main/java/com/yxkj/other/modular/wildcard/erase/transmute.java
参考
泛型擦除:https://blog.csdn.net/Dcwjh/article/details/102832280?utm_medium=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~default-1.no_search_link&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~default-1.no_search_link
逆变与协变:https://zhuanlan.zhihu.com/p/131602691;
https://blog.csdn.net/wangnanwlw/article/details/108711962?utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-1.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~default-1.no_search_link