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


Answers:


200

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

当您针对通用类型或方法编译某些代码时,编译器会弄清您的实际意思(即,类型参数的含义T),并在编译时验证您做的正确,但是发出的代码再次只是在讨论就java.lang.Object-编译器在必要时生成额外的强制类型转换。在执行时,a List<String> 和a List<Date>完全相同;多余的类型信息已被编译器擦除

将此与C#进行比较,在C#中信息将在执行时保留,从而允许代码包含typeof(T)等价于T.class-的表达式,但后者是无效的。(请注意,.NET泛型和Java泛型之间还有其他区别。)在处理Java泛型时,类型擦除是许多“奇数”警告/错误消息的来源。

其他资源:


6
@Rogerio:不,对象不会有不同的泛型类型。该领域知道的类型,但对象没有。
乔恩·斯基特

8
@Rogerio:绝对如此-例如,在执行时很容易找出是否仅提供Object(实际上是弱类型的情况下)提供的内容List<String>。在Java中这是不可行的-您可以发现它是一个ArrayList,而不是原始的通用类型。例如,在串行化/反序列化情况下可能会出现这种情况。另一个示例是容器必须能够构造其通用类型的实例-您必须在Java中分别传递该类型(如Class<T>)。
乔恩·斯基特

6
我从未声称过它总是或几乎总是一个问题-但就我的经验而言,至少在一定程度上是一个经常性的问题。在很多地方,Class<T>仅仅因为Java不保留该信息,我就不得不在构造器(或通用方法)中添加参数。看一下EnumSet.allOf-方法的泛型类型参数就足够了;为什么还需要指定“正常”参数?答:类型擦除。这种事情会污染API。出于兴趣,您是否经常使用.NET泛型?(续)
乔恩·斯基特

5
在使用.NET泛型之前,我发现Java泛型有各种尴尬的地方(通配符仍然令人头疼,尽管“调用者指定的”方差形式肯定具有优势)-但这只是在我使用.NET泛型之后有一阵子,我看到Java泛型有多少模式变得笨拙或不可能。再次是Blub悖论。我并不是说.NET泛型也没有缺点,顺便说一句-不幸的是,无法表达各种类型的关系-但我更喜欢Java泛型。
乔恩·斯基特

5
@Rogerio:您可以使用反射来做很多事情-但是我并没有发现我想要做这些事情的频率几乎是我使用Java泛型无法做到的。我不希望找出一个字段的类型参数几乎一样经常因为我想找出一个实际对象的类型参数。
乔恩·斯基特

41

就像一个旁注一样,这是一个有趣的练习,它实际上是查看编译器执行擦除操作时的操作-使整个概念更容易掌握。有一个特殊的标志,您可以将其传递给编译器以输出已删除了泛型并插入了强制类型转换的Java文件。一个例子:

javac -XD-printflat -d output_dir SomeFile.java

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

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


29

从字面上看,擦除意味着从编译的字节码中删除源代码中存在的类型信息。让我们用一些代码来理解这一点。

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文件中擦除类型后查看代码,但是.class文件仍然具有类型信息。我试过jigawot说,它有效。
坦率的

25

要完成已经非常完整的Jon Skeet的答案,您必须认识到类型擦除的概念源自与Java早期版本的兼容性需求。

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

  • 源代码兼容性(不错...)
  • 二进制兼容性(必须具备!)
  • 迁移兼容性
    • 现有程序必须继续工作
    • 现有库必须能够使用泛型类型
    • 一定有!

原始答案:

因此:

new ArrayList<String>() => new ArrayList()

还有一个更大的命题具体化。Reify是“将抽象概念视为真实”,其中语言构造应该是概念,而不仅仅是句法糖。

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

该语言中的泛型机制提供了编译时(静态)类型检查,但可以通过未检查的强制转换来破坏该机制

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

但是,有时仅靠静态类型检查是不够的,例如:

  • 当将集合传递给第三方库时,必须使库代码不要通过插入错误类型的元素来破坏集合。
  • 程序失败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();
}

2
请注意,可以在不删除类型的情况下实现向后兼容,但是如果没有Java程序员学习新的集合,就不能实现向后兼容。这正是.NET遵循的路线。换句话说,这是重要的第三点。(续)
乔恩·斯基特

15
我个人认为这是近视的错误-它带来了短期优势和长期劣势。
乔恩·斯基特

8

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

曾经提到过通过擦除实现泛型会导致一些令人讨厌的限制(例如no new T[42])。还提到了这样做的主要原因是字节码中的向后兼容性。这(大部分)也是正确的。所生成的字节码-target 1.5与仅经过减糖转换-target 1.4有所不同。从技术上讲,甚至有可能(通过巨大的欺骗手段)在运行时访问通用类型的实例化,从而证明字节码中确实存在某些内容。

更有趣的一点(尚未提出)是,使用擦除实现泛型为高级类型系统可以完成的工作提供了更大的灵活性。一个很好的例子是Scala的JVM实现与CLR。在JVM上,由于JVM本身对通用类型没有任何限制(因为实际上没有这些“类型”),因此有可能直接实现更高种类。这与CLR相反,CLR具有参数实例化的运行时知识。因此,CLR本身必须具有关于应如何使用泛型的一些概念,从而使使用未预期规则扩展系统的尝试无效。结果,Scala在CLR上的高级功能是使用在编译器本身内模拟的怪异擦除形式实现的,

当您想在运行时执行顽皮的事情时,擦除可能会带来不便,但它确实为编译器编写者提供了最大的灵活性。我猜这就是为什么它不会很快消失的部分原因。


6
当您想在执行时执行“顽皮”的事情时,便不会带来不便。这是您想要在执行时做完全合理的事情的时候。实际上,类型擦除允许您做更顽皮的事情-例如将List <String>强制转换为List,然后仅将警告强制转换为List <Date>。
乔恩·斯基特

5

据我了解(作为.NET专家),JVM没有泛型的概念,因此编译器将类型参数替换为Object并为您执行所有强制转换。

这意味着Java泛型不过是语法糖,对于通过引用传递时需要装箱/拆箱的值类型不会提供任何性能改进。


3
Java泛型无论如何都不能表示值类型-没有List <int>这样的东西。然而,没有通通过引用在Java中的话-它是严格按值传递(如该值可以做个参考。)
乔恩斯基特

2

有很好的解释。我仅添加一个示例来说明类型擦除如何与反编译器一起使用。

原始班级

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);


   }
}

2

为什么要使用泛型

简而言之,泛型在定义类,接口和方法时使类型(类和接口)成为参数。与方法声明中使用的更熟悉的形式参数非常相似,类型参数为您提供了一种使用不同输入重复使用相同代码的方法。区别在于形式参数的输入是值,而类型参数的输入是类型。与非泛型代码相比,使用泛型的ode有很多好处:

  • 在编译时进行更强的类型检查。
  • 消除演员阵容。
  • 使程序员能够实现通用算法。

什么是类型擦除

Java语言引入了泛型,以在编译时提供更严格的类型检查并支持泛型编程。为了实现泛型,Java编译器将类型擦除应用于:

  • 如果类型参数不受限制,则将通用类型中的所有类型参数替换为其边界或对象。因此,产生的字节码仅包含普通的类,接口和方法。
  • 必要时插入类型转换,以保持类型安全。
  • 生成桥接方法以在扩展的泛型类型中保留多态。

[NB]-什么是桥接方法?简而言之,对于诸如的参数化接口Comparable<T>,这可能会导致编译器插入其他方法。这些其他方法称为桥接。

清除工作原理

类型的擦除定义如下:从参数化类型中删除所有类型参数,并用擦除其边界的方式替换任何类型变量,如果没有边界则用Object替换,如果有的话将其替换为最左边的边界。多重界限。这里有些例子:

  • 的擦除List<Integer>List<String>List<List<String>>List
  • 的擦除 List<Integer>[]List[]
  • 的擦除 List对于任何原始类型,本身都是类似的。
  • 对于任何原始类型,int的擦除本身就是其本身。
  • 的擦除 Integer本身就是,对于没有类型参数的任何类型都类似。
  • 的擦除T中的定义asList就是Object,因为T 已经没有任何约束。
  • T的定义中的擦除maxComparable因为T 绑定Comparable<? super T>
  • T最终定义为中的擦除maxObject,因为 T已绑定Object&,Comparable<T>而我们将最左边的擦除。

使用泛型时需要小心

在Java中,两个不同的方法不能具有相同的签名。由于泛型是通过擦除实现的,因此还可以得出结论,两种不同的方法不能具有具有相同擦除的签名。一个类不能重载签名具有相同擦除作用的两个方法,并且一个类不能实现具有相同擦除作用的两个接口。

    class Overloaded2 {
        // compile-time error, cannot overload two methods with same erasure
        public static boolean allZero(List<Integer> ints) {
            for (int i : ints) if (i != 0) return false;
            return true;
        }
        public static boolean allZero(List<String> strings) {
            for (String s : strings) if (s.length() != 0) return false;
            return true;
        }
    }

我们希望此代码的工作方式如下:

assert allZero(Arrays.asList(0,0,0));
assert allZero(Arrays.asList("","",""));

但是,在这种情况下,两种方法签名的擦除是相同的:

boolean allZero(List)

因此,在编译时会报告名称冲突。不可能给两个方法使用相同的名称,并试图通过重载来区分它们,因为在擦除之后,不可能将一个方法调用与另一个方法调用区分开。

希望读者会喜欢 :)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.