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

156

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

7个回答

220
这基本上是通过编译器的技巧在Java中实现泛型的方式。编译后的泛型代码实际上只是在你谈论T(或其他类型参数)时使用java.lang.Object - 并且有一些元数据告诉编译器它确实是一个泛型类型。当您针对泛型类型或方法编译一些代码时,编译器会计算出您实际意味着什么(即T的类型参数是什么),并在编译时验证您是否做正确的事情,但发出的代码再次只是以java.lang.Object为术语 - 编译器在必要时生成额外的转换。在执行时间,List和List完全相同;编译器已经抹掉了额外的类型信息。与C#相比,例如保留了执行时间的信息,允许代码包含类似于typeof(T)的表达式,该表达式等效于T.class - 除了后者无效。类型擦除是处理Java泛型时出现许多“奇怪”警告/错误消息的来源。

6
不,这些“对象”不会有不同的泛型类型。这些“字段”知道它们的类型,但是这些“对象”不知道。 - Jon Skeet
8
@Rogerio:毫无疑问,在弱类型情况下,很容易在执行时找出仅以Object形式提供的内容实际上是否为List<String>。在Java中这是不可行的 - 你可以发现它是一个ArrayList,但不知道原始泛型类型是什么。这种情况可能会在序列化/反序列化时发生,例如。另一个例子是容器必须能够构造其泛型类型的实例 - 在Java中必须单独传递该类型(如 Class <T>)。 - Jon Skeet
6
我从未声称它总是或几乎总是成问题 - 但根据我的经验,它至少经常是个问题。在某些场合下,我被迫向构造函数(或泛型方法)添加一个Class<T>参数,仅仅是因为Java无法保留这些信息。例如,看一下EnumSet.allOf- 方法的泛型类型参数应该足够了; 为什么我还需要指定一个“普通”的参数呢?答案:类型擦除。这种情况会污染API。顺便问一下,你使用过.NET泛型吗?(续) - Jon Skeet
5
在开始使用.NET泛型之前,我发现Java的泛型在各种方面都很别扭(通配符仍然很让人头痛,尽管“调用者指定”形式的变异确实有优势),但是只有在我使用了.NET泛型一段时间后,我才看到有多少模式变得别扭或在Java泛型中不可能实现。这又是Blub悖论。顺便说一句,我并不是说.NET泛型没有缺点 - 不幸的是,有各种类型关系无法表达 - 但我远远更喜欢它而不是Java泛型。 - Jon Skeet
5
@Rogerio:反射确实有很多用途,但我通常发现我想要使用Java泛型做的事情比起能够使用反射做的事情更加频繁。我不太需要经常找出一个字段的类型参数,但我经常需要找出实际对象的类型参数。 - Jon Skeet
显示剩余10条评论

42

仅作为旁注,实际上观察编译器执行类型擦除时正在做什么是一个有趣的练习--这可以使整个概念更容易理解。您可以通过传递特殊标志来让编译器输出已经进行了泛型擦除和插入转换的Java文件。例如:

javac -XD-printflat -d output_dir SomeFile.java
-printflat是传递给生成文件的编译器的标志。(而-XD部分则告诉javac将其交给实际执行编译的可执行JAR文件,而不仅仅是javac, 但我离题了...) -d output_dir是必需的,因为编译器需要一个位置来放置新的 .java 文件。
当然,这不仅仅是擦除;编译器执行的所有自动化操作都在这里完成。例如,还会插入默认构造函数,新的foreach-style for循环会被扩展为常规的for循环等等。看到自动发生的小事情真是太好了。

我尝试了上述命令,在反编译的类中仍然可以看到 T 和 E 而不是 object。这个命令在 Java 11 中不起作用,还是 Java 11 改变了类型擦除的工作方式? - Akshay jain

32

擦除字面意思是源代码中存在的类型信息被从编译后的字节码中擦除。让我们通过一些代码来理解这个概念。

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);
        }
    }
}
如果您使用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();

    }
} 

我尝试使用Java反编译器查看经过类型擦除后的.class文件中的代码,但仍然保留了类型信息。我试过了“jigawot”所说的方法,它有效。 - frank

26
为了补充Jon Skeet已经非常完整的回答,你需要意识到类型擦除的概念源于与Java先前版本的兼容性需求。最初在EclipseCon 2007上介绍的兼容性包括以下几点:
- 源代码兼容性(很好有...) - 二进制兼容性(必须有!) - 迁移兼容性
- 现有程序必须继续工作 - 现有库必须能够使用泛型类型 - 必须有!
原始回答:
new ArrayList<String>() => new ArrayList()

有关更大的实体化的提议。实体化意味着“将抽象概念视为真实”,其中语言结构应该是概念,而不仅仅是语法糖。

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

语言中的泛型机制提供了编译时(静态)类型检查,但可以使用未经检查的强制转换来打败这种机制。

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

然而,在以下情况下,仅静态类型检查是不够的:

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

2012年7月更新,近四年后:

现在已经详细说明在 "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();
}

3
请注意,即使不使用类型擦除也可以实现向后兼容,但这需要Java程序员学习一组新的集合。这正是.NET所采取的路线。换句话说,这第三个要点是最重要的。(续前文。) - Jon Skeet
17
我个人认为这是一种目光短浅的错误决策——它带来了短期利益,但长期却会受到损害。 - Jon Skeet

9
补充Jon Skeet的答案...
已经提到,通过擦除实现泛型会导致一些烦人的限制(例如无法使用 new T[42])。同时也提到了为什么要这样做的主要原因是字节码的向后兼容性。这也(大多数情况下)是正确的。使用-target 1.5生成的字节码与仅仅是去掉语法糖的-target 1.4有所不同。从技术上讲,通过极其巧妙的手段甚至可以在运行时获得对泛型类型实例化的访问,证明字节码中确实存在某些东西。
更有趣的一点是(尚未提出),通过擦除实现泛型可以为高级类型系统提供更多的灵活性。一个很好的例子就是Scala的JVM实现和CLR。在JVM上,由于JVM本身对泛型类型没有限制(因为这些“类型”实际上不存在),因此直接实现高阶类型是可能的。这与CLR形成对比,CLR具有参数实例化的运行时知识。因此,CLR本身必须有一些概念来说明如何使用泛型,从而使尝试扩展系统以满足预期规则的尝试无效。结果是,Scala在CLR上的高阶类型是使用编译器内部模拟的一种奇怪的擦除形式来实现的,使它们与普通的.NET泛型不完全兼容。
当你想在运行时做一些恶意操作时,擦除可能会不方便,但它确实为编译器编写者提供了最大的灵活性。我猜这就是为什么它不会很快消失的原因之一。

8
不方便的事情并不是在执行时想要做“淘气”的事情,而是在执行时想要做完全合理的事情。实际上,类型擦除使您能够做更多“淘气”的事情——比如将List<String>转换为List,然后再转换为List<Date>,只会收到警告。 - Jon Skeet

6
据我所知(作为一名.NET开发人员),JVM没有泛型的概念,因此编译器会用Object替换类型参数,并为您执行所有转换操作。
这意味着Java泛型只是语法糖,对于需要通过引用传递时进行装箱/拆箱的值类型不提供任何性能改进。

3
Java 泛型无法表示值类型,例如 List<int> 这样的类型是不存在的。但是,在 Java 中根本没有传递引用 - 它严格按值传递(其中该值可能是一个引用)。 - Jon Skeet

3

有很好的解释。我只是添加一个例子,以展示类型擦除如何与反编译器一起工作。

原始类:

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


public class S<T> {

    T obj; 

    S(T o) {
        obj = o;
    }

    T getob() {
        return obj;
    }

    public static void main(String args[]) {
        List<String> list = new ArrayList<>();
        list.add("Hello");

        // for-each
        for(String s : list) {
            String temp = s;
            System.out.println(temp);
        }

        // stream
        list.forEach(System.out::println);
    }
}

将代码从它的字节码中反编译出来,

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.Consumer;

public class S {

   Object obj;


   S(Object var1) {
      this.obj = var1;
   }

   Object getob() {
      return this.obj;
   }

   public static void main(String[] var0) {

   ArrayList var1 = new ArrayList();
   var1.add("Hello");


   // for-each
   Iterator iterator = var1.iterator();

   while (iterator.hasNext()) {
         String string;
         String string2 = string = (String)iterator.next();
         System.out.println(string2);
   }


   // stream
   PrintStream printStream = System.out;
   Objects.requireNonNull(printStream);
   var1.forEach(printStream::println);


   }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接