《Java核心技术卷I》泛型篇笔记(二) 泛型底层原理——泛型擦除

写在最前:本笔记全程参考《Java核心技术卷I》,添加了一些个人的思考和整理


泛型底层原理

虚拟机没有泛型类型对象,所有对象都将转换为普通类。

1. 类型擦除

定义一个泛型类型时会自动提供一个原始类型(raw type),它的名字就是去掉类型参数之后的泛型类型名。类型变量会被擦除,并替换为其限定类型(或者,对于无限定的变量替换为Object)

例如,Pair<T>的原代码如下:

public class Pair<T> {
    private T first;
    private T second;
    public Pair(T first; T second) {
        this.first = first;
        this.second = second;
    }
    
    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; }
}

对于虚拟机来说,它接触到的是Pair类型擦除后的样子(因为T是无限定类型的,所以以Object替换):

public class Pair {
    private Object first; 
    private Object second;
    public Pair(Object first; Object second) {
        this.first = first;
        this.second = second;
    }
    
    public Object getFirst() { return first; }
    public void setFirst(Object first) { this.first = first; }
    
    public Object getSecond() { return second; }
    public void setSecond(Object second) { this.second = second; }
}

可以看到,类型擦除之后的泛型类其实就是一个普通类,就像java引入泛型之前一样。

程序中可以包含不同的Pair类型,例如Pair<String>或者Pair<Date,不过类型擦除后,只会剩下最原始的Pair类型

就这一点,java和C++的模板有很大的区别,C++会为每一个模板的实例化产生不同的类型,这现象被称为“模板代码碰膨胀”,java不存在这个问题

如果泛型有限定,那么会用第一个限定来替换类型变量。

例如对于Pair<T extends Comparable & Serializable>,泛型擦除后会是这样的:

public class Pair {
    private Comparable first; 
    private Comparable second;
    public Pair(Comparable first; Comparable second) {
        this.first = first;
        this.second = second;
    }
    
    public Comparable getFirst() { return first; }
    public void setFirst(Comparable first) { this.first = first; }
    
    public Comparable getSecond() { return second; }
    public void setSecond(Comparable second) { this.second = second; }
}

为了提高效率,应该将标签接口(即没有任何方法的接口)放在限定列表的末尾。

2. 转换泛型表达式

泛型方法的调用时,编译器除了会擦除返回类型,还会插入强制类型转换,例如:

Pair<Employee> staff = new Pair<>();
...
Employee buddy staff.getFirst();

在上述代码中,编译器做了两件事:

  1. 对原始方法getFirst的调用
  2. 将返回的Object类型强制转换为Employee类型

同理,如果可以直接访问泛型字段(成员变量),那么编译器也会自动插入并执行强制类型转换

3. 转换泛型方法

从上面的例子可以看出,类型擦除会应用在泛型方法中:

public static <T extends Comparable> T min(T a) 
-- 类型擦除后 -->>
public static Comparable min(Comparable a) 

这样子看起来没啥毛病,但实际上却会引起一下问题。

存在的问题

看一下这个继承的子类:

class DataInterval extends Pair<LocalDate> {
    // 重写setSecond方法
    public void setSecond(LocalDate second) {
        if (second.compareTo(getFirst) >= 0) {
            super.setSecond(second);
        }
    }
    // 其他代码...
}

解释一下,DataInterval继承了Pair<LocalDate>,此时Pair<LocalDate>的泛型类型确定了,在类型擦除时,first/second字段以及get/set方法都会修改为LocalDate类型,而DataInterval会继承这些方法(私有字段不继承)。

仔细思考一下,现在DataInterval类有几个setSecond方法?实际上有两个,一个是DataInterval类中重写的setSecond(LocalDate)方法,另一个是从父类Pair<LocalDate>继承来的setSecond(Object)方法(类型擦除之后的方法)

由于多态的特性,这两个方法会起冲突,很明显,LocalDate使Object的子类,如果一个对象可以作为参数传入setSecond(getFirst)方法,那肯定也可以作为参数传入setSecond(Object)方法。实际上,如果不添加其他操作,最后调用的是setSecond(Object)方法

测试方法:

public class ErasureTest {
	public static void main(String[] args) {
		SupperClass father = new Children();
		father.print("a string"); // father
	}
}

class SupperClass {
	public void print(Object o) {
		System.out.println("father");
	}
}

class Children extends SupperClass {
	public void print(String s) { // String也是Object的子类
		System.out.println("children");
	}
}

考虑下面的代码:

DateInterval interval = new DateInterval();
Pair<LocalDate> pair = interval; // 多态,父类型引用指向子类型对象
LocalDate today = LocalDate.now() //获取当前日期 年月日
pair.setSecond(today);

我们希望setSecond方法具有多态性,会调用最合适的那个方法,由于pair对象引用一个DateInterval对象,所以应该调用DateInterval.setSecond。问题在于类型擦除和多态发送了冲突,最终会调用setSecond(Object)方法。

问题解决:桥方法

为了解决这个冲突,编译器自动地在DateInterval类中生成了一个桥方法(bridge method):

public void setSecond(Object second) {
    setSecond((LocalDate) second); //通过强转参数,使其调用setSecond(LocalDate)方法
}

这样子,运行时,程序会执行DateInterval.setSecond(Object)方法,通过这个方法执行setSecond(LocalDate)方法,这正是我们想要的结果。

但是还存在一个疑问,按照上面这个道理,那么DateInterval类还有两个getSecond方法:

public Object getSecond() {} //父类继承来的类型擦除之后的get方法
public LocalDate getSecond() { // 桥方法
    return (LocalDate)super.getSecond();
} 

但是,我们编写代码时不允许这么编写,因为java不支持仅有返回值不同的方法重载,此处仅有返回值不同,按道理是会报错的。

但是!在虚拟机中,会由参数类型和返回值类型共同指定一个方法。因此,编译器可以为两个仅方法返回值不同的方法生成字节码,而虚拟机可以正确的处理这种情况。

下面关于类型擦除和桥方法的代码例子:

public class ErasureTest {
	public static void main(String[] args) {
		Father<String> father = new Childrens();
		father.print1("run!"); // 输出:Father 1 -> run!
		father.print2("run!"); // 输出:Childrens 2 -> run!
	}
}

class Father<T> {
	public void print1(Object data) {
		System.out.println("Father 1 -> " + data);
	}

	public void print2(T data) { // 类型擦除之T会变为Object
		System.out.println("Father 2 -> " + data);
	}
}

class Childrens extends Father<String> {
	// 两个方法print1和print2的实现是一样的
	// 不过print2方法重写了父类方法,print1方法因为参数列表不同,所以不算重写
	public void print1(String data) {
		System.out.println("Childrens 1 -> " + data);
	}

	@Override
	public void print2(String data) {
		System.out.println("Childrens 2 -> " + data);
	}
}

可协变的返回类型的原理

桥方法不仅适用于泛型方法,可协变的返回类型也正是基于桥方法的:

可协变的返回类型指:在覆盖方法时,需要保证方法返回值的兼容性,子类将覆盖方法的返回类型,设置为原返回类型的子类型

这样一来,子类其实就有了两个同名同参数,但是不同返回值的方法。

合成的桥方法会使调用结果变为调用子类中定义的方法。

小结
  1. 虚拟机中没有泛型,只有普通的类和方法
  2. 所有的类型参数都会替换为它们的限定类型
  3. 通过合成的桥方法来保证多态
  4. 为了保证类型安全,必要时会插入强制类型转换

4. 调用遗留代码及消除警告

java中有一些代码,在泛型被设计出来后并没有得到及时更新,而是废弃至今。

例如,Swing图形用户界面工具包提供了一个JSlider类,它的“刻度”(tick)可以定制包含文本和图像的标签。此标签用以下方法设置:

void setLabelTable(Dictionary table)

Dictionary类将横竖类型映射到标签,在java5之前,这个类的Dictionary类是一个Object实例的映射。java5把Dictionary类作为一个泛型类,不过JSlider从未更新,它仍然把Dictionary参数当做原始数据类型。这里就有兼容性问题:填充字典时可以使用泛型类型。

Dictionary<Integer, Component> labelTable = new HashTable();
// ... 对labelTable的一些操作
slider.setLabelTable(labelTable); //警告warning

这里会出现警告合情合理,因为编译器无法确定setLabelTable方法可能会对Dictionary对象做什么操作,毕竟现在JSlider类并不知道你使用了泛型<Integer, Component>,它完全有可能把这个Dictionary对象的键值对换成<String, Component>。我们对此无从得知。

同样的道理,如果我们通过遗留代码获取了某个泛型对象,也会有警告产生:

Dictionary<Integer, Component> labelTable = slider.getLabelTable(); // 警告

注解忽略冲突

好在,SwingJSlider类,在这里只是读取labelTable的信息,而不会改变它的内容,而且就算真的出了差错,最坏就是使出现异常而已。如果我们可以承担这些风险,我们就可以忽略这个警告(但是,即便如此,泛型与遗留代码的兼容性问题依然存在)

即使我们忽略了,但是编译器还是会在那里提示warning,这怎么办呢?

我们可以在引起警告的局部变量上使用**@SuppressWarnings("unchecked")注解**,消除这个变量的所有代码检查

@SuppressWarnings("unchecked")
slider.setLabelTable(labelTable);// 没有警告了

或者,你可以对整个方法添加该注解,只需要在方法声明的上一行添加该注解即可。这样会消除该方法内所有的代码检查。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值