前言
用一篇文章记录泛型的内容显得有点多,所以分成了两部分,本章接着上篇的内容继续整理,文章的内容来源还是 Java 核心技术 卷1 基础知识。
正文
类型擦除
Java 虚拟机中没有泛型的概念,编译器在将 .java 文件编译成 .class 文件的时候,会将代码中的泛型内容擦除。即擦除类型变量,替换为限定类型(无限定的变量替换为 Object)。
例如,泛型类 Pair
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 Object getSecond() {
return second;
}
public void setFirst(Object first) {
first = first;
}
public void setSecond(Object second) {
second = second;
}
}
因为 T 是一个无限定的变量,所以直接用 Object 替换。类型擦除后,Pair 就是一个普通的类。
如果泛型类使用了类型变量的限定,则原始类型用第一个限定的类型变量来替换。例如,声明了如下泛型类:
public class Interval<T extends Comparable & Serializable> implements Serializable {
private T lower;
private T upper;
......
public Interval(T first, T second) {
if (first.compareTo(second) <= 0) { lower = first; upper = second; }
else { lower = second; upper = first; }
}
}
Interval 类型擦除后的内容如下所示:
public class Interval implements Serializable {
private Comparable lower;
private Comparable upper;
public Interval(Comparable first, Comparable second) { ...... }
}
这里有一个思考:如果我将 Interval 类中的限定调换一下,class Interval
如果这样做,原始类型用 Serializable 替换 T,而编译器在必要时要向 Comparable 插入强制类型转换。所以,为了提高效率,建议将标签接口(即没有方法的接口)放在限定列表的末尾。
类型擦除对字节码的影响
先来看第一个情况,当程序调用泛型方法时,如果擦除返回类型,编译器需要插入强制类型转换。例如,看下面的调用代码:
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
擦除 getFirst 的返回类型后将返回 Object 类型。编译器自动插人 Employee 的强制类型转换。也就是说,编译器把这个方法调用翻译为两条虚拟机指令:
- 对原始方法 Pair.getFirst 的调用。
- 将返回的 Object 类型强制转换为 Employee 类型。
第二种情况,通过在类中自动生成一个桥方法(bridge method),以保证擦除类型后的代码仍然具有泛型的“多态性”。例如,DateInterval 继承 Pair,重写 setSecond 和 getSecond 方法:
public class DateInterval extends Pair<LocalDate> {
@Override
public void setSecond(LocalDate second) {
if (second.compareTo(getFirst()) >= 0)
super.setSecond(second);
}
@Override
public LocalDate getSecond() {
return super.getSecond();
}
}
DateInterval 类型擦除后:
public class DateInterval extends Pair {
@Override
public void setSecond(LocalDate second) {
......
}
public LocalDate getSecond() {
......
}
}
Pair 类型擦除后,setSecond 方法的参数类型是 Object,getSecond 方法的返回类型是 Object,而子类 DateInterval 的参数和返回类型是 LocalDate。所以,DateInterval 类中有两个 setSecond 方法,一个是自己的,一个是从 Pair 继承的。
在某些场景中,调用 DateInterval 类中的方法的时候,有可能会调用到从父类继承的那个 setSecond 方法。
我们的本意是重写父类的方法,可是类型擦除后,变成了重载,这样类型擦除和多态就有了冲突。
为了解决这个问题,编译器在 DateInterval 类中自动生成桥方法(bridge method)。我们来看下 DateInterval.class 反编译后的内容:
package test8;
import java.time.LocalDate;
import java.time.chrono.ChronoLocalDate;
// Referenced classes of package test8:
// Pair
public class DateInterval extends Pair
{
public DateInterval()
{
}
public void setSecond(LocalDate second)
{
if(second.compareTo((ChronoLocalDate)getFirst()) >= 0)
super.setSecond(second);
}
public LocalDate getSecond()
{
return (LocalDate)super.getSecond();
}
public volatile void setSecond(Object obj)
{
setSecond((LocalDate)obj);
}
public volatile Object getSecond()
{
return getSecond();
}
}
编译器在 DateInterval 类中新增了两个桥方法:
public volatile void setSecond(Object obj)
public volatile Object getSecond()
桥方法的内部其实是调用了我们自己的 setSecond 方法,
这样在 DateInterval 类中就有两个 getSecond 方法,它们都没有参数,唯一的不同是返回类型。在 Java 中,具有相同参数类型的两个方法是不合法的,但是在虚拟机中,可以用参数类型和返回类型确定一个方法。因此,编译器可能产生两个仅返回类型不同的方法字节码,虚拟机能够正确地处理这一情况。
约束与局限性
不能用基本类型实例化类型参数
创建 Pair 对象时,不能用基本类型替换类型参数 T:
Pair<int> pair = new Pair<>(6, 8);
只能用非基本类型替换类型参数 T:
Pair<Integer> pair = new Pair<>(6, 8);
Java 编译器会对 6 和 8 自动装箱:
Pair pair = new Pair(Integer.valueOf(6), Integer.valueOf(8));
该限制的原因是,类型擦除之后,Pair 类中的 first,second 字段是 Object 类型,而 Object 不能存储 int 值。
泛型类型不能使用 instanceof 关键字和强制类型转换
因为编译器会擦除代码中的所有类型变量,所以运行时无法验证泛型类型。例如:
if (a instanceof Pair<String>) // Error
if (a instanceof Pair<T>) // Error
Pair<String> p = (Pair<String>) a; // Warning--can only test that a is a Pair
试图查询一个对象是否属于某个泛型类型时,倘若使用 instanceof 会得到一个编译器错误,如果使用强制类型转换会得到一个警告。
同样的道理,getClass 方法总是返回原始类型。例如:
Pair<String> stringPair = new Pair<>();
Pair<Employee> employeePair = new Pair<>();
System.out.println(stringPair.getClass() == employeePair.getClass());
打印 true,这是因为两次调用 getClass 都将返回 Pair.class。
不能创建参数化类型的数组
例如,以下代码编译错误:
Pair<String>[] table = new Pair<String>[10]; // Error
数组有一个特性,即使把数组转换为 Object[],它仍然会记住元素的类型,如果试图存储其他类型的元素,就会抛出一个 ArrayStoreException 异常:
Object[] strings = new String[2];
strings[0] = "hi"; // OK
strings[1] = 100; // An ArrayStoreException is thrown.
不过对于泛型类型,擦除会使这种机制无效。我们假设可以创建参数化类型的数组:
Object[] stringLists = new List<String>[]; // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>(); // OK
stringLists[1] = new ArrayList<Integer>(); // An ArrayStoreException should be thrown,
// but the runtime can't detect it.
如果允许参数化列表数组,则上面的代码将无法抛出所需的 ArrayStoreException。
需要说明的是,只是不允许创建这些数组,而声明类型为 Pair
Varargs 警告
上面我们已经了解到,Java 不支持泛型类型的数组。这一节中我们再来讨论一个相关的问题:向参数个数可变的方法传递一个泛型类型的实例。
例如,下面方法的参数个数是可变的:
public static <T> void addAll(Collection<T> coll, T... ts) {
for (T t : ts) coll.add(t);
}
实际上参数 ts 是一个数组,包含提供的所有实参。
执行如下代码,调用该方法:
Collection<Pair<String>> table = new ArrayList<>();
Pair<String> pair1 = new Pair<>();
Pair<String> pair2 = new Pair<>();
addAll(table, pair1, pair2);
为了调用这个方法,Java 虚拟机必须建立一个 Pair
可以采用两种方法来抑制这个警告。一种方法是为包含 addAll 调用的方法增加注解 @SuppressWarnings(“unchecked”)。或者在 Java SE 7 中,还可以用 @SafeVarargs 直接标注 addAll 方法:
@SafeVarargs
public static <T> void addAll(Collection<T> coll, T... ts)
不能实例化类型变量
不能使用像 new T(…),new T[…] 或 T.class 这样的表达式中的类型变量。
例如,下面的 Pair
public Pair() { first = new T(); second = new T(); } // Error
类型擦除将 T 改变成 Object,我们的本意肯定不希望调用 new Object()。 在 Java SE 8 之后,最好的解决办法是让调用者提供一个构造器表达式。
Pair<String> p = Pair.makePair(String::new);
public static <T> Pair<T> makePair(Supplier<T> constr) {
return new Pair<>(constr.get(), constr.get());
}
makePair 方法接收一个 Supplier
比较传统的解决方法是通过反射调用 Class.newInstance 方法来构造泛型对象。另外,表达式 T.class 是不合法的,因为它会擦除为 Object.class,所以需要将 Class 对象作为实参传给 makePair 方法:
Pair<String> p = Pair.makePair(String.class);
public static <T> Pair<T> makePair(Class<T> cl) {
try {
return new Pair<>(cl.newInstance(), cl.newInstance());
} catch (Exception ex) {
return null;
}
}
不能构造泛型数组
就像不能实例化一个泛型实例一样,也不能实例化数组。比如下面的例子:
public static <T extends Comparable> T[] minmax(T[] a) {
T[] mm = new T[2]; // Error
......
}
我们本意是根据传入的不同类型创建不同的数组,但是类型擦除会让这个方法永远只能构造 Comparable[2] 数组。
和上面的处理方法类似,提供一个数组构造器表达式:
String[] ss = ArrayAlg.minmax(String[]::new, "Tom", "Dick", "Harry");
构造器表达式 String[]::new 指示一个函数,给定所需的长度,会构造一个指定长度的 String 数组。
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a) {
T[] mm = constr.apply(2);
......
}
比较老式的方法是利用反射,调用 Array.newInstance:
public static <T extends Comparable> T[] minmax(T... a) {
T[] mm = (T[]) Array.newInstance(a.getClass().getComponentType(), 2);
......
}
泛型类的静态上下文中类型变量无效
不能在静态域或方法中引用类型变量。例如,无法实现如下功能:
public class Singleton<T> {
private static T singleInstance; // Error
public static T getSingleInstance() { // Error
if (singleInstance == null) construct new instance of T
return singleInstance;
}
}
这个限制只存在于泛型类中,假设允许在静态域或方法中引用类型变量,那么就需要考虑一个问题,我们如何为该类型变量指定一个真实类型?
泛型类是在实例化的时候指定真实类型的(Pair
不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类对象。甚至泛型类扩展 Throwable 都是不合法的。例如,以下定义就不能正常编译:
public class Problem<T> extends Exception { /* ... */ } // Error can't extend Throwable
catch 子句中不能使用类型变量。例如,以下方法将不能编译:
public static <T extends Throwable> void doWork(Class<T> t) {
try {
// do work
} catch (T e) { // Error -- can't catch type variable
Logger.global.info(...)
}
}
不过,在异常规范中使用类型变量是允许的。以下方法是合法的:
public static <T extends Throwable> void doWork(T t) throws T { // OK
try {
// do work
} catch (Throwable realCause) {
t.initCause(realCause);
throw t;
}
}
注意擦除后的冲突
当泛型类型被擦除时,无法创建引发冲突的条件。例如,像下面这样将 equals 方法添加到 Pair 类中:
public class Pair<T> {
public boolean equals(T value) {
return first.equals(value) && second.equals(value);
}
}
当定义成 Pair
- boolean equals(String) // defined in Pair
- boolean equals(Object) // inherited from Object
其实,类型擦除后,boolean equals(T) 就是 boolean equals(Object),这时就与 Object.equals 方法发生冲突。补救的方法是重新命名引发错误的方法。
另外一个原则是:
参考
- Java 核心技术 卷1 基础知识 第10版
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/oue6y2 来源:殷建卫 - Java 开发笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。