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

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


当前回答

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

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。

其他回答

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

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。

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

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

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

我也推荐Skeet先生关于Java泛型中擦除的概念是什么?

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

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

注意:与大多数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/

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

示例代码

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