我在甲骨文的网站上读到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>给空方法(Box<T extends Number>)。
API很复杂,但是您可以通过反射API使用getGenericParameterTypes、getGenericReturnType和getGenericType(对于字段)等方法检查此类型信息。
如果有使用泛型类型的代码,编译器会根据需要(在调用者中)插入类型转换来检查类型。泛型对象本身只是原始类型;参数化类型被“擦除”。因此,当您创建一个新的Box<Integer>()时,Box对象中没有关于Integer类的信息。
Angelika Langer的FAQ是我所见过的关于Java泛型的最好参考。
编译器负责在编译时理解泛型。编译器还负责丢弃对泛型类的这种“理解”,这个过程我们称之为类型擦除。所有这些都发生在编译时。
注意:与大多数Java开发人员的想法相反,可以保留编译时类型的信息并在运行时检索这些信息,尽管使用的方式非常有限。换句话说:Java确实以非常有限的方式提供了具象化的泛型。
关于类型擦除
Notice that, at compile-time, the compiler has full type information available but this information is intentionally dropped in general when the byte code is generated, in a process known as type erasure. This is done this way due to compatibility issues: The intention of language designers was providing full source code compatibility and full byte code compatibility between versions of the platform. If it were implemented differently, you would have to recompile your legacy applications when migrating to newer versions of the platform. The way it was done, all method signatures are preserved (source code compatibility) and you don't need to recompile anything (binary compatibility).
关于Java中的具体化泛型
如果需要保留编译时类型信息,则需要使用匿名类。
重点是:在匿名类这种非常特殊的情况下,可以在运行时检索完整的编译时类型信息,换句话说,这意味着:具体化的泛型。这意味着当涉及到匿名类时,编译器不会丢弃类型信息;这些信息保存在生成的二进制代码中,运行时系统允许您检索这些信息。
我写过一篇关于这个主题的文章:
https://rgomes.info/using-typetokens-to-retrieve-generic-parameters/
关于上面文章中描述的技术需要注意的是,大多数开发人员对该技术并不了解。尽管它很好用,但大多数开发人员对这种技术感到困惑或不舒服。如果您有一个共享代码库或计划向公众发布您的代码,我不建议使用上述技术。另一方面,如果您是代码的唯一用户,则可以利用该技术提供的强大功能。
示例代码
上面的文章有示例代码的链接。
类型擦除发生在编译时。类型擦除意味着它将忘记泛型类型,而不是所有类型。此外,仍然会有关于泛型类型的元数据。例如
Box<String> b = new Box<String>();
String x = b.getDefault();
转换为
Box b = new Box();
String x = (String) b.getDefault();
在编译时。你可能会得到警告,不是因为编译器知道什么类型是的泛型,而是相反,因为它知道的不够多,所以它不能保证类型安全。
此外,编译器保留方法调用参数的类型信息,您可以通过反射检索这些信息。
这本指南是我在这方面找到的最好的。
我在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
因此,在简化代码中,实际的参数化类被替换为没有任何类型信息的原始类类型。
作为我的项目的解决方案,我删除了所有的反射调用,并用函数参数传递的显式参数类型替换它们。
类型擦除适用于泛型的使用。类文件中肯定有元数据来说明方法/类型是否是泛型的,以及约束是什么等等。但是当使用泛型时,它们被转换为编译时检查和执行时强制转换。这段代码:
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。