ChatGPT解决这个技术问题 Extra ChatGPT

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

我读到了 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())。如果我们在类路径中使用 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());
    }
}

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

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

w
wchargin

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

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

编辑:为了澄清,编译器确实保留了关于 variable 作为 List<String> 的信息 - 但您仍然无法为列表对象本身找出 T=String


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

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

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

关于类型擦除

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

关于 Java 中的具体化泛型

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

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

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

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

示例代码

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


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

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

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

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

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

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

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


实际上,它是编译到类中的字段和方法的正式泛型类型,即典型的“T”。要获取泛型类型的 real 类型,您必须使用 "anonymous-class trick"
t
try-catch-finally

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

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

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

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


V
Vinko Vrsalovic

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

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

被转换为

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

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

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

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


S
Stephen C

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

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

什么是类型擦除?

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

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

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

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

关于 Java 没有具体化泛型的经常发表的声明也是错误的。

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

什么是物化?

来自Wikipedia

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

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

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

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


p
porfirion

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

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