首页 文章

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

提问于
浏览
214

我读到了Java的类型擦除on Oracle's website .

When does type erasure occur? 在编译时或运行时?当 class 加载?当类被实例化时?

很多站点(包括上面提到的官方教程)都说在编译时会发生类型擦除 . 如果在编译时完全删除了类型信息,那么当调用使用泛型的方法而没有类型信息或错误的类型信息时,JDK如何检查类型兼容性?

请考虑以下示例:Say class 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());
    }
}

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

7 回答

  • 11

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

    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 .

  • 2

    编译器负责在编译时理解泛型 . 编译器还负责在我们称为类型擦除的过程中丢弃这个"understanding"泛型类 . 一切都发生在编译时 .

    Note: 与大多数Java开发人员的信念相反,尽管采用非常有限的方式,仍可以保留编译时类型信息并在运行时检索此信息 . 换句话说: Java does provide reified generics in a very restricted way .

    关于类型擦除

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

    关于Java中的reified通用

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

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

    http://rgomes-info.blogspot.co.uk/2013/12/using-typetokens-to-retrieve-generic.html

    关于上述文章中描述的技术的注释是,对于大多数开发人员而言,该技术是模糊的 . 尽管它工作正常并且运行良好,但大多数开发人员对此技术感到困惑或不安 . 如果您有共享代码库或计划向公众发布代码,我不建议使用上述技术 . 另一方面,如果您是代码的唯一用户,则可以利用此技术为您提供的强大功能 .

    示例代码

    上面的文章包含示例代码的链接 .

  • 88

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

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

    此信息是编译器用于告诉您无法将 Box<String> 传递给 empty(Box<T extends Number>) 方法的信息 .

    API很复杂,但您可以检查它使用getGenericParameterTypesgetGenericReturnType等方法通过反射API输入信息,对于字段,getGenericType .

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

    Angelika Langer's FAQ是我在Java Generics中看到的最佳参考 .

  • 31

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

    泛型由Java编译器实现为称为擦除的前端转换 . 您(几乎)可以将其视为源到源的转换,其中漏洞()的通用版本将转换为非泛型版本 .

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

    我'd also recommend Mr. Skeet'的回答What is the concept of erasure in generics in Java?

  • 1

    类型擦除发生在编译时 . 什么类型的擦除意味着它会忘记泛型类型,而不是每种类型 . 此外,仍然会有关于通用类型的元数据 . 例如

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

    转换为

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

    在编译时 . 您可能会收到警告,不是因为编译器知道通用类型是什么类型,而是相反,因为它不够了解所以它不能保证类型安全 .

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

    这个guide是我在这个主题上发现的最好的 .

  • 6

    我在Android中遇到了类型擦除 . 在 生产环境 中我们使用gradle with 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类型重新复制它们 .

  • 214

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

    在你厌恶之前,请从Wiki回忆一下类型擦除的正确定义

    What is type erasure?

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

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

    所以这是回报,这是正常的自然事物 .

    Java的问题是不同的,并导致它如何实现 .

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

    Java确实有所改进,但由于向后兼容性而导致错误 .

    What is reification?

    来自我们Wiki

    Reification是将计算机程序的抽象概念转换为显式数据模型或用编程语言创建的其他对象的过程 .

    具体化意味着通过专业化将某些抽象(参数类型)转换为具体的(具体类型) .

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

    具有定义的ArrayList:

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

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

    ArrayList<Integer>
    {
        Integer[] elems;
    }
    

    其中 ArrayList<Integer> 确实是一种类型 .

    但是这就是Java does not!!! ,而是他们不断地使用它们的边界来抽象抽象类型,即产生与传递给专业化的参数无关的相同具体类型:

    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中使用泛型方法重载 .

相关问题