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


238

在Oracle网站上了解了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());
    }
}

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



@afryingpan:我的答案中提到的文章详细解释了类型擦除的方式和时间。它还说明了何时保留类型信息。换句话说:与普遍的看法相反,Java中提供了泛型泛型。参见:rgomes.info/using-typetokens-to-retrieve-generic-parameters
理查德·戈麦斯

Answers:


240

类型擦除适用于泛型的使用。有一个在类文件的元数据肯定说的方法/类型是否通用的,什么约束等,但仿制药的时候使用,他们转换成编译时检查和执行时间转换。所以这段代码:

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


6
不可以,即使在使用通用类型时,在运行时也可能有元数据可用。本地变量无法通过反射访问,但是对于声明为“ List <String> l”的方法参数,运行时将存在类型元数据,可通过反射API使用。是的,“类型消除”并非那么简单,很多人认为...
罗杰里奥

4
@Rogerio:我回答您的评论时,我相信您在能够获取变量类型和能够获取对象类型之间感到困惑。即使字段知道对象本身,也不知道其类型参数。
乔恩·斯基特

当然,仅查看对象本身就不会知道它是List <String>。但是对象不只是从无处出现。它们是在本地创建的,作为方法调用参数传入,作为方法调用的返回值返回,或者从某些对象的字段中读取...在所有这些情况下,您都可以在运行时知道泛型是什么,或者隐式地或通过使用Java Reflection API。
罗格里奥(Rogério)

13
@Rogerio:你怎么知道对象从何而来?如果您有类型的参数,List<? extends InputStream>如何知道它在创建时的类型?即使您可以找出引用存储在其中的字段类型,为什么还必须这么做?为什么您应该能够在执行时获得有关对象的所有其余信息,而不能获得其通用类型参数?您似乎正在尝试使类型擦除成为一件很小的事情,但实际上并不会影响开发人员-而我发现在某些情况下这是一个非常重要的问题。
乔恩·斯基特

但是类型擦除是一件很小的事情,实际上并不会影响开发人员!当然,我不能为他人代言,但是以我的经验,这从来没有什么大不了的。实际上,我在Java模拟API(JMockit)的设计中利用了运行时类型信息。具有讽刺意味的是,.NET模拟API似乎没有充分利用C#中可用的通用类型系统。
罗杰里奥2009年

99

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

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

关于类型擦除

请注意,在编译时,编译器具有可用的完整类型信息,但通常在生成字节代码时有意将这种信息丢弃这是在称为类型擦除的过程中进行的。由于存在兼容性问题,因此采用了这种方式:语言设计者的意图是提供平台版本之间的完整源代码兼容性和完整字节代码兼容性。如果以不同的方式实现,则在迁移到平台的较新版本时,您将不得不重新编译旧版应用程序。完成的方式将保留所有方法签名(源代码兼容性),而您无需重新编译任何内容(二进制兼容性)。

关于Java中的泛型泛型

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

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

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

关于以上文章中描述的技术的注释是,该技术对于大多数开发人员来说是晦涩的。尽管这项技术行之有效,但大多数开发人员对该技术感到困惑或不舒服。如果您具有共享的代码库或计划将代码公开发布,则不建议您使用上述技术。另一方面,如果您是代码的唯一用户,则可以利用此技术提供给您的功能。

样例代码

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


1
@ will824:我已经大大改善了答案,并且添加了一些测试用例的链接。干杯:)
理查德·戈麦斯

1
实际上,它们并没有同时保持二进制和源代码的兼容性:oracle.com/technetwork/java/javase/compatibility-137462.html在哪里可以阅读有关其意图的更多信息?Docs说它使用类型擦除,但没有说明原因。
Dzmitry Lazerka

@Richard的确,优秀文章!您可以添加本地类也可以工作,并且在两种情况下(匿名类和本地类),仅在直接访问的情况下保留有关所需类型参数的信息,new Box<String>() {};而在间接访问的情况下则保留有关所需类型参数的void foo(T) {...new Box<T>() {};...}信息,因为编译器不保留以下信息:封闭方法声明。
晏-盖尔Guéhéneuc

我已经修复了文章的损坏链接。我正在慢慢消除自己的生活并回收我的数据。:-)
Richard Gomes

33

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

如果您有一个采用或返回通用类型的方法,则这些类型参数将编译到该类中。

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

该API是复杂的,但你可以通过检查用类似方法反射API这种类型的信息getGenericParameterTypesgetGenericReturnType以及,对田,getGenericType

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

Angelika Langer的FAQ是我所见过的Java泛型的最佳参考。


2
实际上,它是字段和方法的正式泛型类型,被编译到该类中,即典型的“ T”。要获取泛型的型,您必须使用“匿名类技巧”
晏-盖尔Guéhéneuc

13

Java语言中的泛型是有关此主题的非常好的指南。

Java编译器将泛型实现为称为“擦除”的前端转换。您可以(几乎)将其视为源到源的转换,从而将的通用版本loophole()转换为非通用版本。

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

我还建议Skeet先生回答Java中泛型的擦除概念是什么?


6

类型擦除发生在编译时。类型擦除的意思是它将忘记通用类型,而不是每个类型。此外,仍然存在有关通用类型的元数据。例如

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

转换为

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

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

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

指南是我在该主题上找到的最好的指南


6

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

在厌恶之前,请记得从Wiki正确定义类型擦除

什么是类型擦除?

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

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

因此,这是正常的自然回报。

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

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

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

什么是具体化?

从我们的维基

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

归一化意味着通过专门化将抽象的东西(参数类型)转换为具体的东西(混凝土类型)。

我们通过一个简单的例子说明这一点:

具有定义的ArrayList:

ArrayList<T>
{
    T[] elems;
    ...//methods
}

是一个抽象,详细地讲是一个类型构造函数,当专门用于具体类型时,它会被“重新定义”,例如整数:

ArrayList<Integer>
{
    Integer[] elems;
}

哪里ArrayList<Integer>真的是类型。

但这正是 Java所没有的!!!,而是使用边界来不断地抽象化抽象类型,即产生相同的具体类型,而与为进行专门化而传入的参数无关:

ArrayList
{
    Object[] elems;
}

此处使用隐式绑定Object(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中使用。


2

我在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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.