写在最前:本笔记全程参考《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();
在上述代码中,编译器做了两件事:
- 对原始方法
getFirst
的调用 - 将返回的
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);
}
}
可协变的返回类型的原理
桥方法不仅适用于泛型方法,可协变的返回类型也正是基于桥方法的:
可协变的返回类型指:在覆盖方法时,需要保证方法返回值的兼容性,子类将覆盖方法的返回类型,设置为原返回类型的子类型。
这样一来,子类其实就有了两个同名同参数,但是不同返回值的方法。
合成的桥方法会使调用结果变为调用子类中定义的方法。
小结
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型参数都会替换为它们的限定类型
- 通过合成的桥方法来保证多态
- 为了保证类型安全,必要时会插入强制类型转换
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(); // 警告
注解忽略冲突
好在,Swing
的JSlider
类,在这里只是读取labelTable
的信息,而不会改变它的内容,而且就算真的出了差错,最坏就是使出现异常而已。如果我们可以承担这些风险,我们就可以忽略这个警告(但是,即便如此,泛型与遗留代码的兼容性问题依然存在)
即使我们忽略了,但是编译器还是会在那里提示warning,这怎么办呢?
我们可以在引起警告的局部变量上使用**@SuppressWarnings("unchecked")
注解**,消除这个变量的所有代码检查
@SuppressWarnings("unchecked")
slider.setLabelTable(labelTable);// 没有警告了
或者,你可以对整个方法添加该注解,只需要在方法声明的上一行添加该注解即可。这样会消除该方法内所有的代码检查。