我在甲骨文的网站上读到Java的类型擦除。
什么时候发生类型擦除?在编译时还是运行时?什么时候加载类?类何时实例化?
很多网站(包括上面提到的官方教程)都说类型擦除发生在编译时。如果在编译时完全删除了类型信息,当调用使用泛型的方法时没有类型信息或类型信息错误时,JDK如何检查类型兼容性?
考虑下面的例子:假设类A有一个方法,空(Box<?我们编译A.java并获得类文件A.class。
public class A {
public static void empty(Box<? extends Number> b) {}
}
public class Box<T> {}
现在我们创建另一个类B,它使用一个非参数化参数(原始类型)调用方法empty: empty(new Box())。如果在类路径中使用a .class编译B.java, javac足够聪明,可以发出警告。A.class中存储了一些类型信息。
public class B {
public static void invoke() {
// java: unchecked method invocation:
// method empty in class A is applied to given types
// required: Box<? extends java.lang.Number>
// found: Box
// java: unchecked conversion
// required: Box<? extends java.lang.Number>
// found: Box
A.empty(new Box());
}
}
我的猜测是,类型擦除发生在类加载时,但这只是一个猜测。那么什么时候发生呢?
类型擦除适用于泛型的使用。类文件中肯定有元数据来说明方法/类型是否是泛型的,以及约束是什么等等。但是当使用泛型时,它们被转换为编译时检查和执行时强制转换。这段代码:
List<String> list = new ArrayList<String>();
list.add("Hi");
String x = list.get(0);
编译成
List list = new ArrayList();
list.add("Hi");
String x = (String) list.get(0);
在执行时,没有办法找出列表对象的T=String -该信息已消失。
... 但是List<T>接口本身仍然宣称自己是通用的。
编辑:只是为了澄清,编译器确实保留了关于变量是List<String>的信息-但您仍然无法找到列表对象本身的T=String。
类型擦除发生在编译时。类型擦除意味着它将忘记泛型类型,而不是所有类型。此外,仍然会有关于泛型类型的元数据。例如
Box<String> b = new Box<String>();
String x = b.getDefault();
转换为
Box b = new Box();
String x = (String) b.getDefault();
在编译时。你可能会得到警告,不是因为编译器知道什么类型是的泛型,而是相反,因为它知道的不够多,所以它不能保证类型安全。
此外,编译器保留方法调用参数的类型信息,您可以通过反射检索这些信息。
这本指南是我在这方面找到的最好的。
如果字段是泛型类型,则其类型参数将编译到类中。
如果您有一个接受或返回泛型类型的方法,这些类型参数将被编译到类中。
这个信息是编译器用来告诉你你不能传递一个Box<String>给空方法(Box<T extends Number>)。
API很复杂,但是您可以通过反射API使用getGenericParameterTypes、getGenericReturnType和getGenericType(对于字段)等方法检查此类型信息。
如果有使用泛型类型的代码,编译器会根据需要(在调用者中)插入类型转换来检查类型。泛型对象本身只是原始类型;参数化类型被“擦除”。因此,当您创建一个新的Box<Integer>()时,Box对象中没有关于Integer类的信息。
Angelika Langer的FAQ是我所见过的关于Java泛型的最好参考。
我在Android中遇到过的类型擦除。在生产中,我们使用gradle和minify选项。在缩小之后,我得到了致命的例外。我做了一个简单的函数来显示我的对象的继承链:
public static void printSuperclasses(Class clazz) {
Type superClass = clazz.getGenericSuperclass();
Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));
while (superClass != null && clazz != null) {
clazz = clazz.getSuperclass();
superClass = clazz.getGenericSuperclass();
Log.d("Reflection", "this class: " + (clazz == null ? "null" : clazz.getName()));
Log.d("Reflection", "superClass: " + (superClass == null ? "null" : superClass.toString()));
}
}
这个函数有两个结果:
未缩小的代码:
D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: com.example.App.SortedListWrapper<com.example.App.Models.User>
D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: android.support.v7.util.SortedList$Callback<T>
D/Reflection: this class: android.support.v7.util.SortedList$Callback
D/Reflection: superClass: class java.lang.Object
D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null
缩小的代码:
D/Reflection: this class: com.example.App.UsersList
D/Reflection: superClass: class com.example.App.SortedListWrapper
D/Reflection: this class: com.example.App.SortedListWrapper
D/Reflection: superClass: class android.support.v7.g.e
D/Reflection: this class: android.support.v7.g.e
D/Reflection: superClass: class java.lang.Object
D/Reflection: this class: java.lang.Object
D/Reflection: superClass: null
因此,在简化代码中,实际的参数化类被替换为没有任何类型信息的原始类类型。
作为我的项目的解决方案,我删除了所有的反射调用,并用函数参数传递的显式参数类型替换它们。