Java -- 泛型中的类型擦除机制介绍(二)

本文深入探讨Java泛型中的类型擦除机制,解释为何相同泛型方法被视为重载失败,以及类型擦除如何影响泛型代码的表现形式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java -- 泛型中的类型擦除机制介绍(二)

 

上一篇博文中,我们主要介绍了泛型的一些基本使用方法;现在我们来看看泛型的一些较深的特性。

我们也许会声明这样的一组重载方法,来处理String和Integer类型的集合:

class GenericTest {
	public void setList(List<String> list){
		System.out.println("setList(List<String> list) be called...");
	}
	
	public void setList(List<Integer> list){
		System.out.println("setList(List<String> list) be called...");
	}
}

按照我们的想法,这里我们实现了一组重载方法,分别对List<String>和List<Integer>两种声明的集合做不同的处理。但很遗憾,这种重载的写法不被编译器所认可,它会给我们报一个编译错误:

Erasure of method setList(List<Integer>) is the same as another method in type GenericTest

什么意思呢?我们知道一个类中肯定不允许出现两个声明一模一样的方法。而这条编译错误指出:setList(List<String> list)方法擦除后的形式和该类中另一个方法一致,这里的另一个方法当然就是指setList(List<Integer> list)。

我们也许会感到奇怪,编译器为什么会任务setList()的两种重载形式是一致的呢?我们不是明确了不同的类型变量吗?要明白这一点,我们就需要弄清楚泛型中的“类型擦除”这一概念。

类型擦除,顾名思义,我们明确的某个类型变量,如上例中的String,在编译阶段被编译器忽略了,我们看一个示例:

		List<String> list = new ArrayList<String>();
		System.out.println(Arrays.toString(list.getClass().getTypeParameters()));

Class.getTypeParameters()会返回一个TypeVariable对象数组,表示有泛型声明所声明的参数;从该方法描述来看,我们貌似可以得到list对象的参数类型信息(这里期望的是String),但是很遗憾,结果返回的是:

[E]

这样得到的还是一个类型标识符,并不是所期望的具体类型:String。在泛型代码中,我们无法获得任何有关泛型参数类型的信息。从这里我们大概可以猜出,List<Integer>和List<String>经过类型擦除后,在编译器看来是代表相同的类型;既然是类型被擦除,我们可以得出这两种形式都被擦除成了它们的原始类型(原始类型的名字就是删去类型变量后的泛型类型名),即List,这时方法声明完全一致;那么上面的编译错误,也就可以理解了。

在具体讲述类型擦除之前,先介绍一个概念:类型变量的限定,因为泛型类型擦除后的原始类型跟类型变量的限定形式有关。

使用泛型时,我们也许希望对某一类的类型能进行比较操作;那么我们在实例化这种类型时,必须要保证该我们明确的这种类型一定是实现了Comparable<T>接口的。我们怎么在泛型变量中表示这种约束呢?沿用上一篇文章中的一个例子:

class Employee<T extends Comparable<T> & Serializable> {

	T employeeA;
	T employeeB;

	public Employee(T employeeA, T employeeB) {
		super();
		this.employeeA = employeeA;
		this.employeeB = employeeB;
	}

	public T getEmployeeA() {
		return employeeA;
	}

	public void setEmployeeA(T employeeA) {
		this.employeeA = employeeA;
	}

	public T getEmployeeB() {
		return employeeB;
	}

	public void setEmployeeB(T employeeB) {
		this.employeeB = employeeB;
	}

}

/**
 * Generic class without limited type variable
 * 
 * @author xzm
 * @param <T>
 *            Generic type
 */
class Pair<T> {

	private T first;
	private T second;

	public Pair() {
		first = null;
		second = null;
	}

	public Pair(T f, T s) {
		first = f;
		second = s;
	}

	public T getFirst() {
		return first;
	}

	public void setFirst(T first) {
		this.first = first;
	}

	public T getSecond() {
		return second;
	}

	public void setSecond(T second) {
		this.second = second;
	}
}

类Employee<T extends Comparable<T> & Serializable>中的类型变量T进行了额外的条件限定,出现了一条类型变量限定列表:

T extends Comparable<T> & Serializable

意为"T并不是一个任意类型了,它必须是实现了Comparable<T>和Serializable接口的子类"(&用来连接条件限定的各个接口和某个类, ","则是用来类型变量,如"<T, U>")。这样,变量类型T的范围就被限制了,相对来说,它变得更加具体。这时,如果我们在实例化Employee对象时,明确的类型如果没有实现上述限定的两个接口,编译器就会报错。这里只能使用关键字extends,不能使用implements;也许是出于extends关键字更接近子类概念的原因;这样也免于新增一个关键字。注意,类型变量T和条件绑定的类型既可以是类,也可以是接口;这种情景下,也要符合Java单继承、多实现的规范(限定列表中根据需求可以出现多个接口,但只能有一个类)。如果我们用一个类作为条件限定的话,那么这个类一定是位于类型变量限定列表的首位,否则会报编译错误。例如:

class Employee<T extends Pair<T> &Comparable<T> & Serializable>

介绍完类型变量的限定内容后,我们就可以讨论类型擦除后的原生类型这一问题了。类型擦除后的原生类型由编译器决定,跟类型变量的状态有关。这时只有两种情况:类型变量未被限定、类型变量被限定。

如果类型变量未被限定,如Pair的定义,那经过类型擦除后的Pair的原始类型是:

class Pair {

	private Object first;
	private Object second;

	public Pair() {
		first = null;
	}

	public Pair(Object f, Object s) {
		first = f;
		second = s;
	}

	public Object getFirst() {
		return first;
	}

	public void setFirst(Object first) {
		this.first = first;
	}

	public Objectget Second() {
		return second;
	}

	public void setSecond(Object second) {
		this.second = second;
	}
}

对于Employee,原始类型则是:

class Employee {

	Comparable employeeA;
	Comparable employeeB;

	public Employee(Comparable employeeA, Comparable employeeB) {
		super();
		this.employeeA = employeeA;
		this.employeeB = employeeB;
	}

	public Comparable getEmployeeA() {
		return employeeA;
	}

	public void setEmployeeA(Comparable employeeA) {
		this.employeeA = employeeA;
	}

	public Comparable getEmployeeB() {
		return employeeB;
	}

	public void setEmployeeB(Comparable employeeB) {
		this.employeeB = employeeB;
	}
}

无论何时定义一个泛型类型,编译器都自动提供了一个相应的原始类型,编译器也只使用原始类型。原始类型用第一个限定的类型变量来替换(来自类型变量限定列表),如果没有给限定就用Object替换。

依据这条规则,我们得出了上面的Employee和Pair的原始类型。一般地,为了提高效率,我们应该将标签接口(没有方法的接口)放在类型变量限定列表的末尾。

至此,我们回头再看:

class GenericTest {
	public void setList(List<String> list){
		System.out.println("setList(List<String> list) be called...");
	}
	
	public void setList(List<Integer> list){
		System.out.println("setList(List<String> list) be called...");
	}
}

就很好理解编译错误所提示的内容了。

最后我们要记住:“即使擦除在方法或类内部移除了有关实际类型的信息,编译器仍旧可以确保在方法或类中使用的类型的内部一致性。”

下一篇文章中,我们会根据类型擦除这个特性,来讨论一些泛型表达式和泛型方法调用的细节问题。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值