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

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


当前回答

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

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

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

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

其他回答

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

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

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

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

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

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。

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

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

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

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

这个信息是编译器用来告诉你你不能传递一个Box<String>给空方法(Box<T extends Number>)。

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

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

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

术语“类型擦除”并不能真正正确地描述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中不能使用泛型方法重载。