Java泛型类型擦除:何时以及发生了什么?

时间:2008-12-04 06:13:02

标签: java generics type-erasure

我读到了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,它使用非参数化参数(原始类型)调用方法emptyempty(new Box())。如果我们在类路径中使用B.java编译A.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());
    }
}

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

7 个答案:

答案 0 :(得分:227)

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

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对象本身。

答案 1 :(得分:90)

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

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

关于类型擦除

请注意,在编译时,编译器具有完整的类型信息,但在生成字节代码时,在一般称为类型擦除的过程中,有意删除 。由于兼容性问题,这是通过这种方式完成的:语言设计者的意图是提供完整的源代码兼容性和平台版本之间的完全字节代码兼容性。如果以不同方式实现,则在迁移到较新版本的平台时,您必须重新编译旧版应用程序。完成它的方式,保留所有方法签名(源代码兼容性),您不需要重新编译任何东西(二进制兼容性)。

关于Java中的具体化泛型

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

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

http://rgomes-info.blogspot.co.uk/2013/12/using-typetokens-to-retrieve-generic.html

关于上述文章中描述的技术的注释是,对于大多数开发人员来说,该技术是模糊的。尽管它工作正常并且运行良好,但大多数开发人员对此技术感到困惑或不安。如果您有共享代码库或计划向公众发布代码,我不建议使用上述技术。另一方面,如果您是代码的唯一用户,则可以利用此技术为您提供的强大功能。

示例代码

上面的文章提供了示例代码的链接。

答案 2 :(得分:33)

如果您的字段是泛型类型,则其类型参数将编译到类中。

如果您有一个获取或返回泛型类型的方法,那么这些类型参数将被编译到类中。

此信息是编译器用来告诉您无法将Box<String>传递给empty(Box<T extends Number>)方法的信息。

API很复杂,但您可以使用getGenericParameterTypesgetGenericReturnType等方法通过反射API检查此类型信息,对于字段getGenericType,也可以。

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

Angelika Langer's FAQ是我在Java Generics中看到的最佳参考。

答案 3 :(得分:13)

Generics in Java Language是关于此主题的非常好的指南。

  

泛型由Java实现   编译器作为前端转换   叫做擦除。你可以(几乎)思考   它作为源到源   翻译,即通用   版本loophole()转换为   非通用版本。

所以,它是在编译时。 JVM永远不会知道您使用了哪个ArrayList

我还建议Skeet先生回答What is the concept of erasure in generics in Java?

答案 4 :(得分:6)

类型擦除发生在编译时。什么类型的擦除意味着它会忘记泛型类型,而不是每种类型。此外,仍然会有关于通用类型的元数据。例如

Box<String> b = new Box<String>();
String x = b.getDefault();

转换为

Box b = new Box();
String x = (String) b.getDefault();

在编译时。您可能会收到警告,不是因为编译器知道通用类型是什么类型,而是相反,因为它不够了解所以它不能保证类型安全。

此外,编译器会保留方法调用中有关参数的类型信息,您可以通过反射检索这些信息。

guide是我在这个主题上发现的最好的。

答案 5 :(得分:4)

术语“类型擦除”实际上并不是对Java泛型问题的正确描述。 类型擦除本身并不是一件坏事,确实对于性能而言是非常必要的,并且经常用于C ++,Haskell,D等多种语言。

在您厌恶之前,请从Wiki

回顾类型擦除的正确定义

什么是类型擦除?

  

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

类型擦除是指丢弃在设计时创建的类型标签或在编译时推断的类型标签,以使二进制代码中的已编译程序不包含任何类型标签。 对于每种编译为二进制代码的编程语言来说都是这种情况,除非在某些情况下需要运行时标签。这些例外包括所有存在类型(可引用的Java参考类型,多种语言中的任何类型,联合类型)。 清除类型的原因是程序被转换为某种类型的单类型语言(二进制语言仅允许位),因为类型仅是抽象,并声明其值的结构和适当的语义来处理它们。 >

这是正常的自然回报。

Java的问题有所不同,并导致它如何化。

关于Java的常见陈述没有泛型泛型也是错误的。

Java确实进行了验证,但是由于向后兼容而以错误的方式进行了

什么是具体化?

来自我们的Wiki

  

Reification是将有关计算机程序的抽象概念转换为显式数据模型或以编程语言创建的其他对象的过程。

Reification表示通过专业化将抽象的(参数类型)转换为具体的(混凝土类型)

我们通过一个简单的例子对此进行说明:

具有定义的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中使用。

答案 6 :(得分:2)

我在Android中遇到了类型擦除问题。在生产中我们使用gradle with 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

因此,在缩小的代码中,实际的参数化类被替换为没有任何类型信息的原始类类型。 作为我的项目的解决方案,我删除了所有反射调用,并使用函数参数中传递的显式params类型重新复制它们。