我在甲骨文的网站上读到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());
    }
}

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


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

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

它在编译时。JVM永远不会知道您使用了哪个数组列表。

我也推荐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>给空方法(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/

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

示例代码

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


我在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等语言中使用。

在你厌恶之前,请回忆一下维基百科上对字体擦除的正确定义

什么是类型擦除?

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

Type erasure means to throw away type tags created at design time or inferred type tags at compile time such that the compiled program in binary code does not contain any type tags. And this is the case for every programming language compiling to binary code except in some cases where you need runtime tags. These exceptions include for instance all existential types (Java Reference Types which are subtypeable, Any Type in many languages, Union Types). The reason for type erasure is that programs get transformed to a language which is in some kind uni-typed (binary language only allowing bits) as types are abstractions only and assert a structure for its values and the appropriate semantics to handle them.

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

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