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

Posted

技术标签:

【中文标题】Java 泛型类型擦除:何时以及发生啥?【英文标题】:Java generics type erasure: when and what happens?Java 泛型类型擦除:何时以及发生什么? 【发布时间】:2010-09-25 06:41:13 【问题描述】:

我了解到 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.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());
    

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

【问题讨论】:

这个问题的更“通用”版本:***.com/questions/313584/… @afryingpan:我的回答中提到的文章详细解释了类型擦除发生的方式和时间。它还解释了何时保留类型信息。换句话说:物化泛型在 Java 中是可用的,这与普遍的看法相反。见:rgomes.info/using-typetokens-to-retrieve-generic-parameters 【参考方案1】:

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

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&lt;T&gt; 接口本身仍然宣传自己是通用的。

编辑:澄清一下,编译器确实保留了有关 variableList&lt;String&gt; 的信息 - 但您仍然无法找到列表对象本身的 T=String

【讨论】:

不,即使在使用泛型类型时,运行时也可能有可用的元数据。局部变量不能通过反射访问,但对于声明为“List l”的方法参数,在运行时会有类型元数据,可通过反射 API 获得。是的,“类型擦除”并不像许多人想象的那么简单...... @Rogerio:当我回复您的评论时,我相信您在能够获得 variable 的类型和能够获得一个对象。对象本身不知道它的类型参数,即使字段知道。 当然,只看对象本身并不能知道它是一个List。但物体并不是凭空出现的。它们在本地创建,作为方法调用参数传入,作为方法调用的返回值返回,或者从某个对象的字段中读取......在所有这些情况下,您可以在运行时知道泛型类型是什么,或者隐式地或使用 Java 反射 API。 @Rogerio:你怎么知道这个物体是从哪里来的?如果你有一个List&lt;? extends InputStream&gt; 类型的参数,你怎么知道它在创建时是什么类型?即使您可以找出存储引用的字段的类型,为什么必须这样做?为什么您应该能够在执行时获得有关对象的所有其余信息,而不是其泛型类型参数?您似乎试图让类型擦除成为这个不会真正影响开发人员的小事情 - 而我发现在某些情况下它是一个非常重要的问题。 但是类型擦除是一件小事,不会真正影响开发人员!当然,我不能代表别人说话,但根据我的经验,这从来都不是什么大不了的事。实际上,我在设计 Java 模拟 API (JMockit) 时利用了运行时类型信息;具有讽刺意味的是,.NET 模拟 API 似乎很少利用 C# 中可用的泛型类型系统。【参考方案2】:

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

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

关于类型擦除

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

关于 Java 中的具体化泛型

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

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

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

关于上面文章中描述的技术的注意事项是,该技术对于大多数开发人员来说是模糊的。尽管它工作得很好,但大多数开发人员对这项技术感到困惑或不舒服。如果您有共享代码库或计划向公众发布您的代码,我不推荐上述技术。另一方面,如果您是您的代码的唯一用户,您可以利用这项技术为您提供的强大功能。

示例代码

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

【讨论】:

@will824:我大大改进了答案,并添加了一些测试用例的链接。干杯:) 实际上,他们并没有同时保持二进制和源代码兼容性:oracle.com/technetwork/java/javase/compatibility-137462.html 我在哪里可以了解更多关于他们意图的信息?文档说它使用类型擦除,但没有说明原因。 @Richard 确实,优秀的文章!您可以添加本地类也可以工作,并且在这两种情况下(匿名类和本地类),仅在直接访问new Box&lt;String&gt;() ; 的情况下才保留有关所需类型参数的信息,而不是在间接访问void foo(T) ...new Box&lt;T&gt;() ;... 的情况下,因为编译器不保留封闭方法声明的类型信息。 我已经修复了我文章的断开链接。我正在慢慢地去谷歌搜索我的生活并回收我的数据。 :-)【参考方案3】:

如果你有一个泛型类型的字段,它的类型参数被编译到类中。

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

此信息是编译器用来告诉您不能将Box&lt;String&gt; 传递给empty(Box&lt;T extends Number&gt;) 方法的信息。

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

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

Angelika Langer's FAQ 是我见过的关于 Java 泛型的最佳参考。

【讨论】:

其实是formal的泛型类型的字段和方法编译到类中,即典型的“T”。要获取泛型类型的real类型,必须使用"anonymous-class trick"。【参考方案4】:

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

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

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

我还推荐 Skeet 先生在 What is the concept of erasure in generics in Java? 上的回答

【讨论】:

【参考方案5】:

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

在你反感之前,请回想Wikipedia中类型擦除的正确定义

什么是类型擦除?

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

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

所以这是作为回报,很自然的事情。

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

关于 Java 没有具体化泛型的说法也是错误的。

Java 确实实现了具体化,但由于向后兼容性而采用了错误的方式。

什么是物化?

来自Wikipedia

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

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

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

一个有定义的 ArrayList:

ArrayList<T>

    T[] elems;
    ...//methods

是一个抽象,详细来说是一个类型构造函数,当专门化为一个具体类型时,它会被“具体化”,比如 Integer:

ArrayList<Integer>

    Integer[] elems;

ArrayList&lt;Integer&gt; 确实是一个类型。

但这正是Java没有!!!的事情,相反,它们不断地用它们的界限具体化抽象类型,即产生独立于为特化传入的参数:

ArrayList

    Object[] elems;

这里使用隐式绑定对象 (ArrayList&lt;T extends Object&gt; == ArrayList&lt;T&gt;) 进行具体化。

尽管它使泛型数组无法使用并导致原始类型出现一些奇怪的错误:

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】:

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

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

转换为

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

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

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

guide 是我在这个主题上找到的最好的。

【讨论】:

【参考方案7】:

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

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

【讨论】:

以上是关于Java 泛型类型擦除:何时以及发生啥?的主要内容,如果未能解决你的问题,请参考以下文章

Java中的类型擦除与桥方法

[转]类型擦除

关于Java泛型的类型擦除机制问题求教

Java泛型:类型擦除

Simple JavaJava类型擦除机制

27.Android架构-泛型擦除机制