Java中泛型的擦除概念是什么?

Tus*_*shu 138 java generics

Java中泛型的擦除概念是什么?

Jon*_*eet 195

它基本上是通过编译器技巧在Java中实现泛型的方式.编译的通用代码实际上只是java.lang.Object在你谈论的地方T(或其他类型参数)使用 - 并且有一些元数据告诉编译器它确实是泛型类型.

当您针对泛型类型或方法编译某些代码时,编译器会找出您的真正含义(即类型参数T是什么)并在编译时验证您正在做正确的事情,但是发出的代码再次只是会谈就...而言java.lang.Object- 编译器会在必要时生成额外的强制转换.在执行时,a List<String> 和a List<Date>完全相同; 额外的类型信息已被编译器删除.

将此与C#进行比较,其中信息在执行时保留,允许代码包含typeof(T)等效的表达式T.class- 除了后者无效.(请注意,.NET泛型和Java泛型之间存在进一步的差异.)在处理Java泛型时,类型擦除是许多"奇怪"警告/错误消息的来源.

其他资源:

  • @Rogerio:绝对 - 在执行时很容易发现,例如,提供为"Object"(在弱类型场景中)的东西实际上是"List <String>".在Java中,这是不可行的 - 您可以发现它是一个`ArrayList`,但不是原始的泛型类型.例如,这种事情可能出现在序列化/反序列化的情况中.另一个例子是容器必须能够构造其泛型类型的实例 - 您必须在Java中单独传递该类型(作为"Class <T>`"). (8认同)
  • @Rogerio:不,*对象*不会有不同的泛型类型.*字段*知道类型,但对象不知道. (6认同)
  • 我从未声称它总是或几乎总是一个问题 - 但至少*合理*经常是我的经验中的一个问题.有很多地方我不得不将"Class <T>"参数添加到构造函数(或泛型方法),因为Java不保留该信息.例如,查看`EnumSet.allOf` - 方法的泛型类型参数应该足够; 为什么我还需要指定一个"正常"参数?答:键入擦除.这种事情会污染API.出于兴趣,您是否使用过.NET泛型?(继续) (6认同)
  • 在我使用.NET泛型之前,我发现Java泛型在各方面都很尴尬(并且通配符仍然令人头疼,尽管"调用者指定的"方差形式肯定有优势) - 但它只是在我使用.NET泛型之后有一段时间我看到Java泛型有多少模式变得笨拙或不可能.这又是Blub悖论.我不是说.NET泛型也没有缺点,顺便说一下 - 不幸的是,有各种类型的关系无法表达 - 但我更喜欢Java泛型. (5认同)
  • @Rogerio:你可以用反射来做很多事情 - 但我并不倾向于发现我*希望*做的事情几乎与我*不能用Java泛型做的事情一样频繁.我不想像我想要找出实际对象的类型参数那样经常找到字段*的类型参数. (5认同)
  • 是的,但你没有明确指出.像"在执行时,List <String>和List <Date>完全相同"之类的语句可以很容易地被解释为比它实际上更通用.为什么你从来没有提到在运行时实际可用的那么多类型信息?似乎对我有偏见Java ... (3认同)
  • @Rogerio:这句话对我来说似乎很明确.`List <String>`是一个对象,它*与`List <Date>`相同.如果我的意思是`List <String>`*字段*我已经说过了.我在Java和C#中进行了大量编程,并且类型擦除*是一个非常大的痛苦.你似乎是那个试图暗示从来不是问题的人. (2认同)
  • @Rogerio:如果过度使用,那显然是一件坏事 - 但正如你所说,这些东西都有它们的位置......这意味着它们实际上可能是有用的*.但是,我实际上并没有说我正在使用条件逻辑 - 像创建类型参数的新实例这样的东西不是有条件的,但它们依赖于在执行时已知的类型参数. (2认同)

jig*_*wot 40

就像旁注一样,实际看到编译器在执行擦除时正在做什么是一个有趣的练习 - 使整个概念更容易掌握.有一个特殊的标志,你可以通过编译器输出已经删除了泛型和插入的强制转换的java文件.一个例子:

javac -XD-printflat -d output_dir SomeFile.java
Run Code Online (Sandbox Code Playgroud)

-printflat是,被移交到生成文件编译器的标志.(该-XD部分是告诉javac将其交给实际进行编译的可执行jar而不仅仅是javac,但我离题了......)这-d output_dir是必要的,因为编译器需要一些地方来放置新的.java文件.

当然,这不仅仅是擦除; 编译器完成的所有自动操作都在这里完成.例如,还插入了默认构造函数,新的foreach样式for循环扩展为常规for循环等.很高兴看到自动发生的小事情.

  • 哦,非常好; 这就是我长期寻找的东西.谢谢! (2认同)

Par*_*rag 28

擦除,字面意思是源代码中存在的类型信息从编译的字节码中擦除.让我们用一些代码来理解这一点.

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class GenericsErasure {
    public static void main(String args[]) {
        List<String> list = new ArrayList<String>();
        list.add("Hello");
        Iterator<String> iter = list.iterator();
        while(iter.hasNext()) {
            String s = iter.next();
            System.out.println(s);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您编译此代码然后使用Java反编译器对其进行反编译,您将得到类似的结果.请注意,反编译代码不包含原始源代码中存在的类型信息的跟踪.

import java.io.PrintStream;
import java.util.*;

public class GenericsErasure
{

    public GenericsErasure()
    {
    }

    public static void main(String args[])
    {
        List list = new ArrayList();
        list.add("Hello");
        String s;
        for(Iterator iter = list.iterator(); iter.hasNext(); System.out.println(s))
            s = (String)iter.next();

    }
} 
Run Code Online (Sandbox Code Playgroud)

  • 编译和反编译的好主意 (5认同)

Von*_*onC 25

要完成已经非常完整的Jon Skeet的答案,您必须意识到类型擦除的概念源于需要与以前版本的Java兼容.

最初在EclipseCon 2007上展示(不再可用),兼容性包括以下几点:

  • 源兼容性(很高兴......)
  • 二进制兼容性(必须!)
  • 迁移兼容性
    • 现有的计划必须继续发挥作用
    • 现有库必须能够使用泛型类型
    • 一定有!

原始答案:

因此:

new ArrayList<String>() => new ArrayList()
Run Code Online (Sandbox Code Playgroud)

有更大的具体化的命题.实现"将抽象概念视为真实",其中语言结构应该是概念,而不仅仅是语法糖.

我还应该提到checkCollectionJava 6 的方法,它返回指定集合的​​动态类型安全视图.任何插入错误类型元素的尝试都会立即导致ClassCastException.

语言中的泛型机制提供了编译时(静态)类型检查,但是可以使用未经检查的强制转换来破坏此机制.

通常这不是问题,因为编译器会对所有此类未经检查的操作发出警告.

但是,有时单独进行静态类型检查是不够的,例如:

  • 当集合传递给第三方库时,库代码必须通过插入错误类型的元素来破坏集合.
  • 程序因a失败ClassCastException,表示将错误输入的元素放入参数化集合中.不幸的是,异常可以在插入错误元素之后的任何时间发生,因此它通常提供关于问题的真实来源的很少或没有信息.

大约四年后的2012年7月更新:

现在(2012)详细介绍了" API迁移兼容性规则(签名测试) "

Java编程语言使用擦除来实现泛型,这确保了旧版本和通用版本通常生成相同的类文件,除了有关类型的一些辅助信息.二进制兼容性未被破坏,因为可以使用泛型类文件替换遗留类文件,而无需更改或重新编译任何客户端代码.

为了便于与非通用遗留代码接口,还可以使用参数化类型的擦除作为类型.这种类型称为原始类型(Java语言规范3/4.8).允许原始类型还可确保源代码的向后兼容性.

根据这个,java.util.Iterator该类的以下版本是二进制和源代码向后兼容:

Class java.util.Iterator as it is defined in Java SE version 1.4:

public interface Iterator {
    boolean hasNext();
    Object next();
    void remove();
}

Class java.util.Iterator as it is defined in Java SE version 5.0:

public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}
Run Code Online (Sandbox Code Playgroud)

  • 我个人认为这是一个短视的错误 - 它给了短期优势和长期劣势. (13认同)
  • 请注意,可以在没有类型擦除的情况下实现向后兼容性,但是没有Java程序员学习一组新的集合.这正是.NET走的路.换句话说,这是第三个重要的子弹.(继续.) (2认同)

Dan*_*wak 8

补充已经补充的Jon Skeet答案......

已经提到通过擦除来实现泛型导致一些恼人的限制(例如,否new T[42]).还有人提到,以这种方式做事的主要原因是字节码的向后兼容性.这也是(大多数)真实的.生成的字节码-target 1.5与仅脱糖的cast -target 1.4略有不同.从技术上讲,甚至可以(通过巨大的技巧)在运行时获得对泛型类型实例的访问,证明字节码中确实存在某些东西.

更有趣的一点(尚未提出)是使用擦除实现泛型在高级类型系统可以实现的方面提供了更多的灵活性.一个很好的例子就是Scala的JVM实现与CLR.在JVM上,由于JVM本身不对泛型类型施加任何限制(因为这些"类型"实际上不存在),因此可以直接实现更高类型.这与CLR形成对比,CLR具有参数实例化的运行时知识.因此,CLR本身必须具有如何使用泛型的一些概念,从而使用未预料到的规则扩展系统的尝试无效.因此,Scala在CLR上的更高种类是使用在编译器本身内模拟的奇怪形式的擦除来实现的,

当你想在运行时做顽皮的东西时,擦除可能会很不方便,但它确实为编译器编写者提供了最大的灵活性.我猜这是为什么它不会很快消失的部分原因.

  • 当你想在执行时做"顽皮"的事情时,不方便.当你想在执行时完成合理的事情时.事实上,类型擦除允许你做更多的事情 - 例如将List <String>转换为List,然后仅使用警告将List <Date>转换为List <Date>. (6认同)

And*_*nan 5

据我所知(作为.NET人员),JVM没有泛型的概念,因此编译器用Object替换类型参数并为您执行所有的转换.

这意味着Java泛型只是语法糖,并且不会为通过引用传递时需要装箱/取消装箱的值类型提供任何性能改进.

  • Java泛型无论如何都不能表示值类型 - 没有List <int>这样的东西.但是,Java中根本没有pass-by-reference - 它严格按值传递(其中该值可能是一个引用.) (3认同)