在Java的泛型中,擦除是什么概念?
仅作为旁注,实际上观察编译器执行类型擦除时正在做什么是一个有趣的练习--这可以使整个概念更容易理解。您可以通过传递特殊标志来让编译器输出已经进行了泛型擦除和插入转换的Java文件。例如:
javac -XD-printflat -d output_dir SomeFile.java
-printflat
是传递给生成文件的编译器的标志。(而-XD
部分则告诉javac
将其交给实际执行编译的可执行JAR文件,而不仅仅是javac
, 但我离题了...) -d output_dir
是必需的,因为编译器需要一个位置来放置新的 .java 文件。for
循环会被扩展为常规的for
循环等等。看到自动发生的小事情真是太好了。擦除字面意思是源代码中存在的类型信息被从编译后的字节码中擦除。让我们通过一些代码来理解这个概念。
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();
}
}
new ArrayList<String>() => new ArrayList()
有关更大的实体化的提议。实体化意味着“将抽象概念视为真实”,其中语言结构应该是概念,而不仅仅是语法糖。
我还应该提到Java 6的checkCollection
方法,它返回指定集合的动态类型安全视图。任何尝试插入错误类型的元素都会立即导致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();
}
new T[42]
)。同时也提到了为什么要这样做的主要原因是字节码的向后兼容性。这也(大多数情况下)是正确的。使用-target 1.5生成的字节码与仅仅是去掉语法糖的-target 1.4有所不同。从技术上讲,通过极其巧妙的手段甚至可以在运行时获得对泛型类型实例化的访问,证明字节码中确实存在某些东西。有很好的解释。我只是添加一个例子,以展示类型擦除如何与反编译器一起工作。
原始类:
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);
}
}
Object
形式提供的内容实际上是否为List<String>
。在Java中这是不可行的 - 你可以发现它是一个ArrayList
,但不知道原始泛型类型是什么。这种情况可能会在序列化/反序列化时发生,例如。另一个例子是容器必须能够构造其泛型类型的实例 - 在Java中必须单独传递该类型(如Class <T>
)。 - Jon SkeetClass<T>
参数,仅仅是因为Java无法保留这些信息。例如,看一下EnumSet.allOf
- 方法的泛型类型参数应该足够了; 为什么我还需要指定一个“普通”的参数呢?答案:类型擦除。这种情况会污染API。顺便问一下,你使用过.NET泛型吗?(续) - Jon Skeet