Java泛型类型擦除:何时发生,发生什么?

我读过Java的类型擦除在甲骨文的网站上

什么时候发生类型擦除?在编译时还是运行时?什么时候加载类?类何时实例化?

很多网站(包括上面提到的官方教程)都说类型擦除发生在编译时。如果在编译时完全删除了类型信息,当调用使用泛型的方法时没有类型信息或类型信息错误时,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())。如果我们在类路径中编译B.javaA.class, 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());
}
}

我的猜测是,类型擦除发生在类加载时,但这只是一个猜测。那么什么时候发生呢?

108224 次浏览

Java语言中的泛型是关于这个主题的一个非常好的指南。

泛型由Java实现 编译器作为前端转换 被称为擦除。你(几乎)会思考 来源对来源 翻译,由此泛化 loophole()的版本被转换为 非通用版本。

它在编译时。JVM永远不会知道你使用的是哪一个ArrayList

我也推荐Skeet先生在Java中泛型中的擦除是什么概念?上的回答

类型擦除适用于泛型的使用。类文件中肯定有元数据来说明方法/类型是否是泛型,以及约束条件是什么等等。但是当泛型为使用时,它们会转换为编译时检查和执行时强制类型转换。这段代码:

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>传递给empty(Box<T extends Number>)方法的。

这个API很复杂,但是你可以通过反射API使用getGenericParameterTypesgetGenericReturnTypegetGenericType等方法来检查这个类型信息。

如果有使用泛型类型的代码,编译器会根据需要(在调用者中)插入类型转换来检查类型。泛型对象本身只是原始类型;参数化类型被“擦除”。因此,当你创建一个new Box<Integer>()对象时,在Box对象中没有关于Integer类的信息。

Angelika Langer的常见问题解答是我所见过的关于Java泛型的最好的参考。

编译器负责在编译时理解泛型。编译器还负责丢弃对泛型类的这种“理解”,这个过程我们称之为类型擦除。所有这些都发生在编译时。

注意:与大多数Java开发人员的信念相反,尽管以非常有限的方式保存编译时类型的信息并在运行时检索这些信息是可能的。换句话说:Java确实以非常有限的方式提供了具象化的泛型

关于类型擦除

注意,在编译时,编译器有完整的类型信息可用,但当字节码生成时,在一个称为类型擦除的过程中,这些信息被故意丢弃在一般情况下。这样做是因为兼容性问题:语言设计者的意图是在平台的不同版本之间提供完整的源代码兼容性和完整的字节码兼容性。如果以不同的方式实现,则在迁移到平台的新版本时必须重新编译遗留应用程序。这样做的方式是,所有的方法签名都被保留(源代码兼容性),你不需要重新编译任何东西(二进制兼容性)。

关于Java中的具体化泛型

如果你需要保持编译时类型信息,你需要使用匿名类。 重点是:在匿名类这种非常特殊的情况下,可以在运行时检索完整的编译时类型信息,换句话说,这意味着:具体化的泛型。这意味着当涉及到匿名类时,编译器不会丢弃类型信息;此信息保存在生成的二进制代码中,运行时系统允许您检索此信息

我写过一篇关于这个主题的文章:

https://rgomes.info/using-typetokens-to-retrieve-generic-parameters/

关于上面文章中描述的技术需要注意的是,大多数开发人员对该技术并不了解。尽管它很好用,但大多数开发人员对这种技术感到困惑或不舒服。如果您有一个共享代码库或计划向公众发布您的代码,我不建议使用上述技术。另一方面,如果您是代码的唯一用户,则可以利用该技术提供的强大功能。

示例代码

上面的文章有示例代码的链接。

我在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
因此,在简化代码中,实际的参数化类被替换为没有任何类型信息的原始类类型。 作为我的项目的解决方案,我删除了所有的反射调用,并将它们替换为函数参数传递的显式参数类型

“类型擦除”;并不是对Java泛型问题的正确描述。 类型擦除本身并不是一件坏事,事实上,它对于性能来说是非常必要的,并且经常用于多种语言,如c++, Haskell, D.

在你厌恶之前,请回忆一下维基百科中类型擦除的正确定义

什么是类型擦除?

类型擦除是指在程序在运行时执行之前,从程序中删除显式类型注释的加载时过程

类型擦除意味着丢弃在设计时创建的类型标记或在编译时推断的类型标记,以便二进制代码中的已编译程序不包含任何类型标记。 除了某些需要运行时标记的情况外,每一种编程语言都可以编译成二进制代码。例如,这些例外包括所有存在类型(可子类型的Java引用类型、多种语言中的任意类型、联合类型)。 类型擦除的原因是程序被转换为某种单一类型(二进制语言只允许位)的语言,因为类型只是抽象,并断言其值的结构和适当的语义来处理它们

所以这是回报,很正常的事情。

Java的问题是不同的,是由它如何具体化引起的。

通常认为Java没有具象化泛型的说法也是错误的。

Java确实具体化了,但由于向后兼容,以一种错误的方式。

什么是物化?

维基百科

具象化是将关于计算机程序的抽象思想转化为用编程语言创建的显式数据模型或其他对象的过程。

物化是指通过专门化将抽象的东西(参数类型)转化为具体的东西(具体类型)。

我们用一个简单的例子来说明这一点:

有定义的数组列表:

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中不能使用泛型方法重载。