Answers:
基本上,这是通过编译器欺骗在Java中实现泛型的方式。编译后的通用代码实际上只使用java.lang.Object
您谈论的任何地方T
(或其他一些类型参数),并且有一些元数据告诉编译器它确实是通用类型。
当您针对通用类型或方法编译某些代码时,编译器会弄清您的实际意思(即,类型参数的含义T
),并在编译时验证您做的正确,但是发出的代码再次只是在讨论就java.lang.Object
-编译器在必要时生成额外的强制类型转换。在执行时,a List<String>
和a List<Date>
完全相同;多余的类型信息已被编译器擦除。
将此与C#进行比较,在C#中信息将在执行时保留,从而允许代码包含typeof(T)
等价于T.class
-的表达式,但后者是无效的。(请注意,.NET泛型和Java泛型之间还有其他区别。)在处理Java泛型时,类型擦除是许多“奇数”警告/错误消息的来源。
其他资源:
Object
(实际上是弱类型的情况下)提供的内容List<String>
。在Java中这是不可行的-您可以发现它是一个ArrayList
,而不是原始的通用类型。例如,在串行化/反序列化情况下可能会出现这种情况。另一个示例是容器必须能够构造其通用类型的实例-您必须在Java中分别传递该类型(如Class<T>
)。
Class<T>
仅仅因为Java不保留该信息,我就不得不在构造器(或通用方法)中添加参数。看一下EnumSet.allOf
-方法的泛型类型参数就足够了;为什么还需要指定“正常”参数?答:类型擦除。这种事情会污染API。出于兴趣,您是否经常使用.NET泛型?(续)
就像一个旁注一样,这是一个有趣的练习,它实际上是查看编译器执行擦除操作时的操作-使整个概念更容易掌握。有一个特殊的标志,您可以将其传递给编译器以输出已删除了泛型并插入了强制类型转换的Java文件。一个例子:
javac -XD-printflat -d output_dir SomeFile.java
的-printflat
是,被移交到生成文件编译器的标志。(-XD
部分是告诉javac
将其交给实际执行编译的可执行jar的过程,而不是公正的javac
,但我离题了……)这-d output_dir
是必需的,因为编译器需要放置一些地方来放置新的.java文件。
当然,这不仅仅是擦除。编译器所做的所有自动工作都在这里完成。例如,还插入了默认构造函数,新的foreach样式的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();
}
}
jigawot
说,它有效。
要完成已经非常完整的Jon Skeet的答案,您必须认识到类型擦除的概念源自与Java早期版本的兼容性需求。
最初在EclipseCon 2007上展示(不再可用),兼容性包括以下几点:
原始答案:
因此:
new ArrayList<String>() => new ArrayList()
还有一个更大的命题具体化。Reify是“将抽象概念视为真实”,其中语言构造应该是概念,而不仅仅是句法糖。
我还应该提到checkCollection
Java 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();
}
补充已经补充的Jon Skeet答案...
曾经提到过通过擦除实现泛型会导致一些令人讨厌的限制(例如no new T[42]
)。还提到了这样做的主要原因是字节码中的向后兼容性。这(大部分)也是正确的。所生成的字节码-target 1.5与仅经过减糖转换-target 1.4有所不同。从技术上讲,甚至有可能(通过巨大的欺骗手段)在运行时访问通用类型的实例化,从而证明字节码中确实存在某些内容。
更有趣的一点(尚未提出)是,使用擦除实现泛型为高级类型系统可以完成的工作提供了更大的灵活性。一个很好的例子是Scala的JVM实现与CLR。在JVM上,由于JVM本身对通用类型没有任何限制(因为实际上没有这些“类型”),因此有可能直接实现更高种类。这与CLR相反,CLR具有参数实例化的运行时知识。因此,CLR本身必须具有关于应如何使用泛型的一些概念,从而使使用未预期规则扩展系统的尝试无效。结果,Scala在CLR上的高级功能是使用在编译器本身内模拟的怪异擦除形式实现的,
当您想在运行时执行顽皮的事情时,擦除可能会带来不便,但它确实为编译器编写者提供了最大的灵活性。我猜这就是为什么它不会很快消失的部分原因。
有很好的解释。我仅添加一个示例来说明类型擦除如何与反编译器一起使用。
原始班级
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);
}
}
为什么要使用泛型
简而言之,泛型在定义类,接口和方法时使类型(类和接口)成为参数。与方法声明中使用的更熟悉的形式参数非常相似,类型参数为您提供了一种使用不同输入重复使用相同代码的方法。区别在于形式参数的输入是值,而类型参数的输入是类型。与非泛型代码相比,使用泛型的ode有很多好处:
什么是类型擦除
Java语言引入了泛型,以在编译时提供更严格的类型检查并支持泛型编程。为了实现泛型,Java编译器将类型擦除应用于:
[NB]-什么是桥接方法?简而言之,对于诸如的参数化接口Comparable<T>
,这可能会导致编译器插入其他方法。这些其他方法称为桥接。
清除工作原理
类型的擦除定义如下:从参数化类型中删除所有类型参数,并用擦除其边界的方式替换任何类型变量,如果没有边界则用Object替换,如果有的话将其替换为最左边的边界。多重界限。这里有些例子:
List<Integer>
,List<String>
和List<List<String>>
是List
。List<Integer>[]
是List[]
。List
对于任何原始类型,本身都是类似的。Integer
本身就是,对于没有类型参数的任何类型都类似。T
中的定义asList
就是Object
,因为T
已经没有任何约束。T
的定义中的擦除max
是Comparable
因为T
绑定Comparable<? super T>
。T
最终定义为中的擦除max
是Object
,因为
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)
因此,在编译时会报告名称冲突。不可能给两个方法使用相同的名称,并试图通过重载来区分它们,因为在擦除之后,不可能将一个方法调用与另一个方法调用区分开。
希望读者会喜欢 :)