Java编程思想之【泛型擦除】

Java编程思想之【泛型擦除】文章目录前言一 简单泛型 1 定义 2 作用 3 泛型二 泛型擦除 1 擦除的神秘之处迁移兼容性擦除的问题擦除补偿前言还记得几年去一间公司面试的时候 面试官问的技术方面的问题 其中一个就是关于擦除的问题 当时的我第一次接触面试有点紧张而且对擦除这个术语还不太了解 说白了当时我就是一个技术小白 现在也差不多啦 所以支支吾吾没怎么回答上来 幸好本人人品爆表 没回答上来也顺利入职了 由于当时没回答上来的尴尬情景依然历历在目 所以这次写这篇博文的目的以弥补技术上的模糊点


前言

还记得几年前去一间公司面试的时候,面试官问的技术方面的问题,其中一个就是关于擦除的问题,当时的我第一次接触面试有点紧张而且对擦除这个术语还不太了解(说白了当时我就是一个技术小白,现在也差不多啦 ╮( ̄▽  ̄)╭ ),所以支支吾吾没怎么回答上来,幸好本人人品爆表,没回答上来也顺利入职了\( ^ ▽ ^ )/。

由于当时没回答上来的尴尬情景依然历历在目,所以这次写这篇博文的目的以弥补技术上的模糊点为主,如果以下表述上有误请各位技术大佬加以指正,灰常感谢! ( * ^ _ ^ * )

一、简单泛型

这边先简述一下泛型的理论知识。

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

(0)
上一篇 2026年3月26日 下午8:21
下一篇 2026年3月26日 下午8:21


相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号