前言
还记得几年前去一间公司面试的时候,面试官问的技术方面的问题,其中一个就是关于擦除的问题,当时的我第一次接触面试有点紧张而且对擦除这个术语还不太了解(说白了当时我就是一个技术小白,现在也差不多啦 ╮( ̄▽  ̄)╭ ),所以支支吾吾没怎么回答上来,幸好本人人品爆表,没回答上来也顺利入职了\( ^ ▽ ^ )/。
由于当时没回答上来的尴尬情景依然历历在目,所以这次写这篇博文的目的以弥补技术上的模糊点为主,如果以下表述上有误请各位技术大佬加以指正,灰常感谢! ( * ^ _ ^ * )
一、简单泛型
这边先简述一下泛型的理论知识。
1.定义
在程序编码中一些包含类型参数的类型,也就是说泛型的参数只可以代表类,不能代表个别对象。
2.作用
泛型的出现,主要原因是为了创造容器类。相对于数组而言,容器类更加灵活。事实上,所有程序在运行时都要求你持有一大堆对象,所以容器类算得上最具重用性的类库之一。
3.泛型
在 Java SE5 (JDK1.5) 以后,新增了一个类型参数 T ,与其使用Object,我们更喜欢暂时不指定类型,而是稍后再决定具体使用什么类型,要达到这个目的,需要使用到类型参数 T ,下面例子中,T 就是类型参数:
public class Holder<T> {
private T a; public Holder(T a) {
this.a = a; } public void set(T a) {
this.a = a; } public T get() {
return a; } }(~ o ~)~zZ
二、泛型擦除
1.擦除的神秘之处
比较两种不同泛型的ArrayList,代码如下:
/ * @Author: Tony Peng / public class EraseTypeEquivalence {
public static void main(String[] args) {
Class stringType = new ArrayList<String>().getClass(); Class integerType = new ArrayList<Integer>().getClass(); System.out.println("类型参数是否相同:" + ((stringType == integerType) ? "相同" : "不相同")); } }~(@ ^_^ @)~
下面示例是对这个谜题的一个补充:
class Frob {
} class Fnorkle {
} class Quark<Q> {
} class Particle<POSITION, MOMENTUM> {
} / * @Author: Tony Peng / public class LostInformation {
public static void main(String[] args) {
List<Frob> list = new ArrayList<>(); Map<Frob, Fnorkle> map = new HashMap<>(16); Quark<Fnorkle> quark = new Quark<>(); Particle<Long, Double> particle = new Particle<>(); System.err.println(Arrays.toString(list.getClass().getTypeParameters())); System.err.println(Arrays.toString(map.getClass().getTypeParameters())); System.err.println(Arrays.toString(quark.getClass().getTypeParameters())); System.err.println(Arrays.toString(particle.getClass().getTypeParameters())); } }(*@ ο @*)
Java泛型是使用擦除来实现的,这意味着当你在使用泛型时,任何具体的类型信息都被擦除了。因此List
和List
在运行时事实上是相同类型。这两种形式都被
擦除成他们的“原生”类型,即List。
这边补充一下常用的被泛型化的集合类,如下:
| 集合类 | 泛型定义 |
|---|---|
| ArrayList | ArrayLiset |
| HashMap | HashMap |
| HashSet | HashSet |
| Vector | Vector |
下面代码简单演示一下 泛化边界 遇到的问题:
/ * @Author: Tony Peng / class HasF {
public void f() {
System.out.println("HasF.f()"); } } //这边必须重用extends关键字来确定边界,否则调用obj.f()会报错,找不到方法 class Manipulator<T extends HasF> {
private T obj; public Manipulator(T x) {
obj = x; } public void manipulate() {
obj.f(); } } public class Manipulation {
public static void main(String[] args) {
HasF hf = new HasF(); Manipulator<HasF> manipulator = new Manipulator<>(hf); manipulator.manipulate(); } }(⊙ o ⊙)
由于有了擦除,如果没有协助泛型类重用extends关键字给定了泛型类边界,Java编译器无法将manipulate()必须能够在obj上调用f()这一需求映射到HasF拥有f()这一事实上。
我们说泛型类型参数将擦除到它的第一个边界,我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面示例一样,T擦除到了HasF,就好像在类的声明中用HasF替换了T一样。
迁移兼容性
在基于擦除的实现中,泛型类型被当作第二类类型处理,即不能在某些重要的上下文环境中使用的类型。泛型类型只有在静态类型检查期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如,诸如List
这样的类型注解都将被擦除为List,而普通的类型变量在未指定边界的情况下将被擦除为Object。
擦除的核心动机是它使得泛化的客户端可以用非泛化的类库来使用,反之亦然,这经常被称为“迁移兼容性”。
擦除的问题
擦除主要的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下,将泛型融入Java语言。
擦除的代价是显著的。泛型不能用于显示地引用运行时类型的操作之中,例如转型、instanceof和new表达式。因为所有关于参数的类型信息都丢失了,无论何时,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来好像拥有有关参数的类型信息而已。
如果你编写了下面这样的代码段:
class Foo<T>{
T var; }
那么,看起来当你在创建Foo的实例时;
Foo<Cat> f = new Foo<Cat>();
class Foo中代码应该知道现在工作于Cat之上,而泛型语法也在强烈暗示:在整个类中的各个地方,类型T都在被替换。但事实并非如此,无论何时,当你在编写这个类的代码时,必须提醒自己:“不,它只是一个Object。”
另外,擦除和迁移兼容性意味着,使用泛型并不是强制的,如下代码演示:
/ * @Author: Tony Peng / class GenericBase<T>{
private T element; public void set(T arg){
arg = element; } public T get(){
return element; } } class Derived1<T> extends GenericBase<T>{
} class Derived2 extends GenericBase{
} // class Derived3 extends GenericBase
{} // java: 意外的类型 // 需要: 不带限制范围的类或接口 // 找到: ? public class ErasureAndInheritance {
public static void main(String[] args) {
Derived2 d2 = new Derived2(); Object obj = d2.get(); d2.set(obj); } }(*^ ‧ ^*)
可以推断,Derived3产生错误意味着编译器期望得到一个原生基类。
2.擦除的补偿
擦除丢失了在泛型代码中执行某些操作的能力。任何运行时需要知道确切类型信息的操作都将无法工作。
演示代码如下:
/ * @Author: Tony Peng / public class Erased<T> {
private final int SIZE = 100; public void f(Object arg) {
if (arg instanceof T){
} // Error T var = new T(); // Error T[] array = new T(); // Error T[] array = (T[]) new Object[SIZE]; // Unchecked warning } }(*∩ _ ∩*)
class Building {
} class House extends Building {
} / * @Author: Tony Peng / public class ClassTypeCapture<T> {
Class<T> kind; public ClassTypeCapture(Class<T> kind) {
this.kind = kind; } public boolean f(Object arg) {
return kind.isInstance(arg); } public static void main(String[] args) {
ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class); System.out.println("Building: " + ctt1.f(new Building())); System.out.println("House: " + ctt1.f(new House())); } }(*> . <*)
在class Erase
中对创建一个new T()的尝试将无法实现,部分原因是因为擦除,而另一部分原因是因为编译器不能验证T具有默认(无参)构造器。
Java中解决方案是传递一个工厂对象,并使用它来创建新的实例。最便利的工厂对象就是Class对象,因此如果使用类型标签,那么你就可以使用newInstance()来创建这个类型的新对象,演示代码如下:
class ClassAsFactory<T> {
T x; public ClassAsFactory(Class<T> kind) {
try {
x = kind.newInstance(); } catch (Exception e) {
throw new RuntimeException(e); } } } class Employee {
} / * @Author: Tony Peng / public class InstantiateGenericType {
public static void main(String[] args) {
ClassAsFactory<Employee> fe = new ClassAsFactory<Employee>(Employee.class); System.out.println("ClassAsFactory
succeed"
); try {
ClassAsFactory<Integer> fi = new ClassAsFactory<Integer>(Integer.class); } catch (Exception e) {
System.out.println("ClassAsFactory
failed"
); } } }(๑ ¯∀¯ ๑)
interface FactoryI<T> {
T create(); } class Foo2<T>{
private T x; public <F extends FactoryI<T>> Foo2(F factory){
x = factory.create(); } } class IntegerFactory implements FactoryI<Integer>{
@Override public Integer create(){
return new Integer(0); } } class Widget {
public static class Factory implements FactoryI<Widget>{
@Override public Widget create(){
return new Widget(); } } } / * @author Tony Peng */ public class FactoryConstraint {
public static void main(String[] args) {
new Foo2<Integer>(new IntegerFactory()); new Foo2<Widget>(new Widget.Factory()); } }╰(* °▽° *)╯
注意,这确实只是传递Class
的一种变体。两种方式都传递了工厂对象,Class
碰巧是内建的工厂对象,而上面的方式创建了一个显式的工厂对象,但是你却获得了编译器检查。
另一种方法是模板方法设计模式。在下面的示例中,get()是模板方法,而create()是在子类中定义的,用来产生子类类型的对象:
abstract class GenericWithCreate<T> {
final T element; GenericWithCreate() {
element = create(); } abstract T create(); } class X {
} class Creator extends GenericWithCreate<X>{
@Override X create(){
return new X(); } void f(){
System.out.println(element.getClass().getSimpleName()); } } / * @author Tony Peng */ public class CreatorGeneric {
public static void main(String[] args) {
Creator c = new Creator(); c.f(); } }φ(≧ ω ≦*)♪
泛型数组
正如你在class Erase
中所见,不能创建泛型数组。一般的解决方案是在任何想要创建泛型数组的地方都使用ArrayList:
/ * @Author: Tony Peng / public class ListOfGenerics<T> {
private List<T> array = new ArrayList<T>(); public void add(T item) {
array.add(item); } public T get(int index) {
return array.get(index); } }φ(゜ ▽ ゜*)♪
这里你将获得数组行为,以及由泛型提供的编译期的类型安全。
有时,你仍旧希望创建泛型类型的数组(例如,ArrayList内部使用的是数组)。有趣的是,可以按照编译器喜欢的方式来定义一个引用,例如:
class Generic<T> {
} / * @author Tony Peng */ public class ArrayOfGenericReference {
static Generic<Integer>[] gia; }( ̄ ‘i  ̄;)
编译器将接受这个警告,而不会产生任何警告。但是,永远都不能创建这个确切类型的数组(包括类型参数),因此这有一点令人困惑。既然所有数组无论它们持有的类型如何,都具有相同的结构(每个数组槽位的尺寸和数组的布局),那么看起来你应该能够创建一个Object数组,并将其转型为所希望的数组类型。事实上这可以编译,但是不能运行,它将产生ClassCase-Exception:
/ * @Author: Tony Peng / public class ArrayOfGeneric {
static final int SIZE = 100; static Generic<Integer>[] gia; public static void main(String[] args) {
//这段代码运行会报ClassCastException异常 gia = (Generic<Integer>[]) new Object[SIZE]; gia = (Generic<Integer>[]) new Generic[SIZE]; System.out.println(gia.getClass().getSimpleName()); gia[0] = new Generic<Integer>(); //java: 不兼容的类型: java.lang.Object无法转换为Generic
gia[1] = new Object(); //java: 不兼容的类型: Generic
无法转换为Generic
gia[2] = new Generic<Double>(); } }(ノ へ  ̄、)
让我们看一个更复杂的示例。考虑一个简单的泛型数组包装器:
/ * @Author: Tony Peng / public class GenericArray<T> {
private T[] array; public GenericArray(int sz) {
array = (T[]) new Object[sz]; } public void put(int index, T item) {
array[index] = item; } public T get(int index) {
return array[index]; } public T[] rep() {
return array; } public static void main(String[] args) {
GenericArray<Integer> gai = new GenericArray<Integer>(10); //这里会报ClassCastException异常 Integer[] ia = gai.rep(); //这样写就不会报异常 Object[] oa = gai.rep(); } }(๑´ ㅂ `๑)
与前面相同,我们并不能声明T[] array = new T[sz],因此我们创建了一个对象数组,然后将其转型。
rep()方法将返回T[],它在main()中将用于gai,因此应该是Integer[],但是如果调用它,并尝试着将结果作为Integer[]引用来捕获,就会得到ClassCastException,这还是因为实际的运行时类型是Object[]。
因为有了擦除,数组的运行时类型就只能是Objec[]。如果我们立即将其转型为T[],那么在编译期该数组的实际类型就将丢失,而编译器可能会错过某些潜在的错误检查。正因为这样,最好是在集合内部使用Object[],然后当你使用的数组元素时,添加一个对T的转型,让我们看看这是如何作用于GenericArray示例的:
/ * @Author: Tony Peng / public class GenericArray2<T> {
private Object[] array; public GenericArray2(int sz) {
array = new Object[sz]; } public void put(int index, T item) {
array[index] = item; } public T get(int index) {
return (T) array[index]; } public T[] rep() {
return (T[]) array; } public static void main(String[] args) {
GenericArray2<Integer> gai = new GenericArray2<Integer>(10); for (int i = 0; i < 10; i++) {
gai.put(i, i); } for (int i = 0; i < 10; i++) {
System.out.print(gai.get(i) + " "); } System.out.println(); try {
Integer[] ia = gai.rep(); } catch (Exception e) {
System.err.println(e); } } }(ー `´ ー)
对于新代码,应该传递一个类型标记。在这种话情况下,GenericArray看起来会像下面这样:
/ * @Author: Tony Peng / public class GenericArrayWithTypeToken<T> {
private T[] array; public GenericArrayWithTypeToken(Class<T> type, int sz) {
array = (T[]) Array.newInstance(type, sz); } public void put(int index, T item) {
array[index] = item; } public T get(int index) {
return array[index]; } public T[] rep() {
return array; } public static void main(String[] args) {
//这样就能运行了 GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<Integer>(Integer.class, 10); Integer[] ia = gai.rep(); } }(lll¬ ω ¬)
类型标记Class
被传递到构造器中,以便从擦初中恢复,使得我们可以创建需要的实际类型的数组。一旦我们获得了实际类型,就可以返回它,并获得想要的结果,就像在main()中看到的那样。该数组的运行时类型是确切类型T[]。
遗憾的是,如果查看Java SE5标准类库中的源代码,你就会看到从Object数组到参数化类型的转型遍及各处。例如,下面是经过整理和简化后的从Collection中复制ArrayList的构造器:
public ArrayList(Collection c) {
size = c.size(); elementData = (E[])new Object[size]; c.toArray(elementData); }
如果你通读ArrayList.java,就会发现它充满了这种转型。
Neal Gafter(Java SE5的领导开发者之一)在他的博客中指出,在重写Java类库时,他十分懒散,而我们不应该像他那样。Neal还指出,在不破坏现有接口的情况下,他将无法修改某些Java类库代码。因此,即使在Java类库源代码中出现了某些惯用法,也不能表示这就是正确的解决之道。当查看类库代码时,你能认为它就是应该在自己代码中遵循的示例。
三、结语
以上内容均参考自《Java编程思想》这本Java圣经。
这次边工作边学习,写这篇大概用了一个礼拜的时间,大概把以前关于擦除相关知识的模糊点给补上了。
即使自己有一定的工作经验了,基础知识仍还有很多需要加强的地方,虽然工作也能学习,因时间紧迫,所获得的知识笼统,只知其一不知其二,只有遇到问题时才深入研究,但知识体系紧锣密鼓,一环扣一环,代码学习之路渊远流长,工作之余不忘学习。
发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/177079.html原文链接:https://javaforall.net
