目 录CONTENT

文章目录

Java基础知识:泛型的类型擦除、逆变与协变

Dioxide-CN
2022-02-01 / 0 评论 / 4 点赞 / 34 阅读 / 8,568 字
温馨提示:
本文最后更新于 2022-04-21,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

定义有如下代码:

public class Main {
	public static void main(String[] args) {
		List<String> stringList = new ArrayList<>();
	}
}

**思考:**泛型类型被擦除是否可以通过反射机制来继续获取泛型类型的信息?–> 可以

从字节码角度对泛型擦除进行分析:

Constant pool:
{
	public static void main(java.lang.String[])
		descriptor: ([Ljava/lang/String;)V
		flags: ACC_PUBLIC, ACC_STATIC
		Code:
			stack=2, locals=2, args_size=1
			0: new           // #2 class java/util/ArrayList
			3: dup
			4: invokespecial // #3 Method java/util/ArrayList."<init>":()V
			7: astore_1
			8: return
		LocalVariableTypeTable:
			Start Length Slot Name Signature
			    8      1    1 stringList Ljava/util/List<Ljava/lang/String;>;
}

在 Code 中: 其中 #2 创建的是 ArrayList 对象,而不是 String 类型的 ArrayList 因而该泛型类型被擦除。
在 LocalVariableTypeTable中: 有一个 Name 等于 stringList 的变量,记录了该对象的泛型信息。

若将泛型信息附加在类上:

class IntList extends ArrayList<Integer> {
	List<String> toStringList() {
		return new ArrayList<>();
	}
}

编译并解析后得到如下结果:

class IntList extends java.util.ArrayList<java.lang.Integer>
Constant pool:
	#14 = Utf8 ()Ljava/util/List<Ljava/lang/String;>;
	#15 = Utf8 Ljava/util/ArrayList<Ljava/lang/Integer;>;
{
	java.util.List<java.lang.String> toStringList();
		descriptor: ()Ljava/util/List;
		flags:
		Code:
			...
		Signature: #14 //()Ljava/util/List<Ljava/lang/String;>;
}
Signature: #15 //Ljava/util/ArrayList<Ljava/lang/Integer;>;
SourceFile: "Main java"

第一个 Signature #14 : 指向来常量池中的 String 类型的 ArrayList 记录了 toString 方法返回值的泛型信息。

第二个 Signature #15 : 指向来常量池中的 Int 类型的 ArrayList 是其父类以及其泛型信息。

总结:泛型类型擦除 ≈ 没有擦除,无论是局部变量中传入的泛型还是类定义上携带的泛型,只要传入了泛型,那么在生成的字节码文件中必然会额外记录这些泛型的具体信息。

对于不同的对象可以通过不同的反射机制来进一步获取被擦除的泛型类型:

(一) 对于挂载在类上的泛型信息,可以通过来获取泛型信:

IntList.class.getGenericSuperclass();

(二) 对于挂载在函数返回类型上的泛型信息,可以通过如下方法来获取泛型信息:

IntList.class.getDeclaredMethod("toStringList").getGenericReturnType();

(三) 对于挂载在局部变量上的泛型信息,可以通过操作字节码工具类(如:javaassist)来获取泛型信息:

ClassPool.getDefault().get("Main")
	.getMethod("main","([Ljava/lang/String;)V")
	.getMethodInfo2().getCodeAttribute()
	.getAttribute("LocalVariableTypeTable");

jvm 为了兼容低版本的 code 部分的指令,将 code 中的泛型信息去除掉了 ==> 即所谓的泛型擦除。

泛型的逆变

定义有如下方法:

interface Filter<E> {
	public boolean test(E element);
}
//根据传入的filter过滤器过滤列表并返回被过滤的元素
public static <E> List<E> removeIf(List<E> list, Filter<E> filter) {
	List<E> removeList = new ArrayList<>();
	for(E e : list) {
		if(filter.test(e)) {
			removeList.add(e);
		}
	}
	list.removeAll(removeList);
	return removeList;
}

现有一个Double类型的列表想通过该方法过滤掉其中值大于100的元素:

List<Double> doubleList = new ArrayList<Double>();

removeIf(doubleList, filter);

Filter<Double> filter = new Filter<Double>() {
	@Override
	public boolean test(Double element) {
		return element > 100;
	}
}

现有假想,对于不同数据类型(Short,Integer)的List若都想调用该Filter过滤器对象则需要定义不同数据类型的过滤器实现方法 -> 但同时在JDK1.5之后对所有数据类型进行了包装,因此所有数据类型的父类都属于Number类,则有假想代码如下:

Filter<Number> filter = new Filter<Number>() {
	@Override
	public boolean test(Number element) {
		return element > 100;
	}
}

很显然:这是不行的,其中 filter 参数会发生报错!此时就需要使用泛型的逆变操作。通过对泛型 <E> 增加通配符 ? super 来对泛型进行逆变操作:

interface Filter<E> {
	public boolean test(E element);
}

public static <E> List<E> removeIf(List<E> list, Filter<? super E> filter) {
	List<E> removeList = new ArrayList<>();
	for(E e : list) {
		if(filter.test(e)) {
			removeList.add(e);
		}
	}
	list.removeAll(removeList);
	return removeList;
}

原本的继承关系

NumberDouble

逆变后的继承关系

继承逆转? Super DoubleNumber

因此 Number 类型的 filter 过滤类可以认为是逆变之后的 Double 类型的 Filter 的子类型。因此,赋值变为合法。通过逆变,可以让泛型的约束变得更加宽松。
协变不同,逆变放宽的是对父类的约束,而协变放宽的是对子类的约束。
但同样,逆变放宽类型约束是存在一定代价的:

List<? super Double> list = new ArrayList<Number>();
//再也无法从函数返回值中得到这个繁星的类型
Double number = list.get(0); //编译不通过
Object number = list.get(0); //只能作为顶层级的Object类

泛型的协变使用的是 ? extends 通配符,使得子类型的泛型对象可以进行赋值,但同样会失去调用 add 存储功能时传递该泛型对象的能力:

//泛型的协变
List<? extends Number> list = new ArrayList<Double>();
list.add(1.0); //无法进行add

总结:

//泛型的协变
List<? extends Number> list = new ArrayList<Double>();
list.add(1.0); //无法进行add

//泛型的逆变
List<? super Double> list = new ArrayList<Number>();
list.get(0); //无法进行get

逆变与协变的使用场景:

  1. 当一个对象只作为泛型的生产者,也就是只取泛型的情况下,可以用 ? extends

    • 例如 JDK 中 ArrayList 的集合构造法中就是使用了协变:
    public ArrayList(@NotNull @Flow(sourcelsContainer = true)Collection<? extends E> c) {
    	Object[] a = c.toArray();
    	if((size = a.length) != 0) {
    		if(c.getClass() == ArrayList.class) {
    			elementData = a;
    		} else {
    			elementData = Array.copyOf(a, size, Object[].class);
    		}
    	} else {
    		//replace with empty array
    		elementData = EMPTY_ELEMENTDATA;
    	}
    }
    
  2. 而当确定对象值作为泛型的消费者,也就是需要调用传入泛型参数的方法时,可以用 ? super

    • 例如 JDK 中 ArrayList 的 removeIf 方法就是使用了逆变:
    public boolean removeIf(Predicate<? super E> filter) {
    	checkForComodification();
    	int oldSize = root.size;
    	boolean modified = root.removeIf(filter, offset, offset + size);
    	if(modified)
    		updateSizeAndModCound(root.size - oldSize);
    	return modified;
    }
    
4

评论区