我读到了 Java 的类型擦除 on Oracle's website。
什么时候发生类型擦除?在编译时还是运行时?什么时候加载类?什么时候实例化类?
很多网站(包括上面提到的官方教程)都说类型擦除发生在编译时。如果在编译时完全删除了类型信息,那么在没有类型信息或类型信息错误的情况下调用使用泛型的方法时,JDK如何检查类型兼容性?
考虑以下示例:假设类 A
有一个方法 empty(Box<? extends Number> b)
。我们编译 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>
接口本身仍然宣传自己是通用的。
编辑:为了澄清,编译器确实保留了关于 variable 作为 List<String>
的信息 - 但您仍然无法为列表对象本身找出 T=String
。
编译器负责在编译时理解泛型。在我们称为类型擦除的过程中,编译器还负责丢弃对泛型类的这种“理解”。一切都发生在编译时。
注意:与大多数 Java 开发人员的看法相反,可以保留编译时类型信息并在运行时检索此信息,尽管方式非常有限。换句话说:Java 确实以非常有限的方式提供了具体化的泛型。
关于类型擦除
请注意,在编译时,编译器具有可用的完整类型信息,但通常在生成字节码时故意删除此信息,该过程称为类型擦除。这样做是因为兼容性问题:语言设计者的目的是提供完整的源代码兼容性和平台版本之间的完整字节码兼容性。如果以不同的方式实现,则在迁移到新版本的平台时,您将不得不重新编译旧应用程序。这样做的方式是保留所有方法签名(源代码兼容性),您不需要重新编译任何东西(二进制兼容性)。
关于 Java 中的具体化泛型
如果需要保留编译时类型信息,则需要使用匿名类。关键是:在匿名类的非常特殊的情况下,可以在运行时检索完整的编译时类型信息,换句话说,这意味着:具体化的泛型。这意味着当涉及匿名类时,编译器不会丢弃类型信息;此信息保存在生成的二进制代码中,运行时系统允许您检索此信息。
我写了一篇关于这个主题的文章:
https://rgomes.info/using-typetokens-to-retrieve-generic-parameters/
关于上面文章中描述的技术的注意事项是,该技术对于大多数开发人员来说是模糊的。尽管它工作得很好,但大多数开发人员对这项技术感到困惑或不舒服。如果您有共享代码库或计划向公众发布您的代码,我不推荐上述技术。另一方面,如果您是您的代码的唯一用户,您可以利用这项技术为您提供的强大功能。
示例代码
上面的文章有示例代码的链接。
new Box<String>() {};
的情况下而不是在间接访问 void foo(T) {...new Box<T>() {};...}
的情况下保留有关所需类型参数的信息,因为编译器不保留封闭方法声明的类型信息。
如果您有一个泛型类型的字段,则其类型参数将编译到类中。
如果您有一个采用或返回泛型类型的方法,则这些类型参数将编译到类中。
该信息是编译器用来告诉您不能将 Box<String>
传递给 empty(Box<T extends Number>)
方法的信息。
API 很复杂,但您可以通过反射 API 使用 getGenericParameterTypes
、getGenericReturnType
和对于字段的 getGenericType
等方法检查此类型信息。
如果您有使用泛型类型的代码,编译器会根据需要(在调用者中)插入强制类型转换以检查类型。泛型对象本身只是原始类型;参数化类型被“擦除”。因此,当您创建 new Box<Integer>()
时,Box
对象中没有关于 Integer
类的信息。
Angelika Langer's FAQ 是我见过的关于 Java 泛型的最佳参考。
Generics in Java Language 是关于这个主题的一个非常好的指南。
Java 编译器将泛型实现为称为擦除的前端转换。您可以(几乎)将其视为源到源的翻译,从而将 loophole() 的通用版本转换为非通用版本。
所以,它在编译时。 JVM 永远不会知道您使用了哪个 ArrayList
。
我还推荐 Skeet 先生在 What is the concept of erasure in generics in Java? 上的回答
类型擦除发生在编译时。类型擦除意味着它将忘记泛型类型,而不是所有类型。此外,仍然会有关于泛型类型的元数据。例如
Box<String> b = new Box<String>();
String x = b.getDefault();
被转换为
Box b = new Box();
String x = (String) b.getDefault();
在编译时。您可能会收到警告,不是因为编译器知道泛型是什么类型,而是相反,因为它知道的不够多,因此无法保证类型安全。
此外,编译器确实保留了有关方法调用参数的类型信息,您可以通过反射检索这些信息。
这个 guide 是我在这个主题上找到的最好的。
术语“类型擦除”并不是对 Java 泛型问题的正确描述。类型擦除本身并不是一件坏事,实际上它对于性能非常必要,并且经常用于 C++、Haskell、D 等多种语言中。
在你反感之前,请回忆一下 Wikipedia 中类型擦除的正确定义
什么是类型擦除?
类型擦除是指加载时过程,在程序运行时执行之前,通过该过程从程序中删除显式类型注释
类型擦除意味着丢弃在设计时创建的类型标签或在编译时推断的类型标签,使得二进制代码中的编译程序不包含任何类型标签。对于编译为二进制代码的每种编程语言都是这种情况,除非在某些情况下您需要运行时标记。这些例外包括例如所有存在类型(可子类型的 Java 引用类型、许多语言中的任何类型、联合类型)。类型擦除的原因是程序被转换为某种单一类型的语言(二进制语言只允许位),因为类型只是抽象,并为其值断言结构和处理它们的适当语义。
所以这是作为回报,很自然的事情。
Java 的问题是不同的,并且是由它具体化的方式引起的。
关于 Java 没有具体化泛型的经常发表的声明也是错误的。
Java 确实实现了具体化,但由于向后兼容性而采用了错误的方式。
什么是物化?
物化是将关于计算机程序的抽象概念转化为显式数据模型或以编程语言创建的其他对象的过程。
具体化是指通过专业化将抽象的东西(参数类型)转换为具体的东西(具体类型)。
我们通过一个简单的例子来说明这一点:
具有定义的 ArrayList:
ArrayList<T>
{
T[] elems;
...//methods
}
是一个抽象,详细来说是一个类型构造函数,当使用具体类型专门化时,它会被“具体化”,比如 Integer:
ArrayList<Integer>
{
Integer[] elems;
}
其中 ArrayList<Integer>
确实是一种类型。
但这正是 Java 没有做的事情!!!,相反,它们不断地用它们的界限来具体化抽象类型,即产生相同的具体类型,而不依赖于为专门化传入的参数:
ArrayList
{
Object[] elems;
}
这里用隐式绑定对象(ArrayList<T extends Object>
== ArrayList<T>
)具体化。
尽管它使泛型数组无法使用并导致原始类型出现一些奇怪的错误:
List<String> l= List.<String>of("h","s");
List lRaw=l
l.add(new Object())
String s=l.get(2) //Cast Exception
它会引起很多歧义,因为
void function(ArrayList<Integer> list){}
void function(ArrayList<Float> list){}
void function(ArrayList<String> list){}
参考相同的功能:
void function(ArrayList list)
因此泛型方法重载不能在 Java 中使用。
我在 Android 中遇到过类型擦除。在生产中,我们使用带有 minify 选项的 gradle。缩小后,我遇到了致命的异常。我制作了简单的函数来显示我的对象的继承链:
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<? extends InputStream>
类型的参数,您如何知道它在创建时是什么类型?即使您可以找出存储引用的字段的类型,为什么必须这样做?为什么您应该能够在执行时获得有关对象的所有其余信息,但不能获得其泛型类型参数?您似乎试图让类型擦除成为这个不会真正影响开发人员的小事情 - 而我发现在某些情况下它是一个非常重要的问题。