前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java基础知识:泛型的类型擦除、逆变与协变

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

作者头像
DioxideCN
发布2022-08-05 19:37:08
7360
发布2022-08-05 19:37:08
举报
文章被收录于专栏:用户4480853的专栏

定义有如下代码:

代码语言:javascript
复制
public class Main {
	public static void main(String[] args) {
		List<String> stringList = new ArrayList<>();
	}
}

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

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

代码语言:javascript
复制
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 的变量,记录了该对象的泛型信息。

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

代码语言:javascript
复制
class IntList extends ArrayList<Integer> {
	List<String> toStringList() {
		return new ArrayList<>();
	}
}

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

代码语言:javascript
复制
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 是其父类以及其泛型信息。

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

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

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

代码语言:javascript
复制
IntList.class.getGenericSuperclass();

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

代码语言:javascript
复制
IntList.class.getDeclaredMethod("toStringList").getGenericReturnType();

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

代码语言:javascript
复制
ClassPool.getDefault().get("Main")
	.getMethod("main","([Ljava/lang/String;)V")
	.getMethodInfo2().getCodeAttribute()
	.getAttribute("LocalVariableTypeTable");

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

泛型的逆变

定义有如下方法:

代码语言:javascript
复制
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的元素:

代码语言:javascript
复制
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类,则有假想代码如下:

代码语言:javascript
复制
Filter<Number> filter = new Filter<Number>() {
	@Override
	public boolean test(Number element) {
		return element > 100;
	}
}

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

代码语言:javascript
复制
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;
}

原本的继承关系

逆变后的继承关系

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

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

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

代码语言:javascript
复制
//泛型的协变
List<? extends Number> list = new ArrayList<Double>();
list.add(1.0); //无法进行add

总结:

代码语言:javascript
复制
//泛型的协变
List<? extends Number> list = new ArrayList<Double>();
list.add(1.0); //无法进行add

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

逆变与协变的使用场景:

当一个对象只作为泛型的生产者,也就是只取泛型的情况下,可以用 ? extends ; 例如 JDK 中 ArrayList 的集合构造法中就是使用了协变:

代码语言:javascript
复制
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;
	}
}
而当确定对象值作为泛型的消费者,也就是需要调用传入泛型参数的方法时,可以用 ? 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;
}

而当确定对象值作为泛型的消费者,也就是需要调用传入泛型参数的方法时,可以用 ?super ;

例如 JDK 中 ArrayList 的 removeIf 方法就是使用了逆变:

代码语言:javascript
复制

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;
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 泛型的逆变
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档