专栏首页Android开发实战深入理解Java泛型(三)-泛型擦除及其相关内容

深入理解Java泛型(三)-泛型擦除及其相关内容

我们下面看一个例子:

Class<?> class1=new ArrayList<String>().getClass();
Class<?> class2=new ArrayList<Integer>().getClass();
System.out.println(class1);        //class java.util.ArrayList
System.out.println(class2);        //class java.util.ArrayList
System.out.println(class1.equals(class2));    //true

我们看输出发现,class1和class2居然是同一个类型ArrayList,在运行时我们传入的类型变量String和Integer都被丢掉了。Java语言泛型在设计的时候为了兼容原来的旧代码,Java的泛型机制使用了“擦除”机制。我们来看一个更彻底的例子:

class Table {}
class Room {}
class House<Q> {}
class Particle<POSITION, MOMENTUM> {}
//调用代码及输出
List<Table> tableList = new ArrayList<Table>();
Map<Room, Table> maps = new HashMap<Room, Table>();
House<Room> house = new House<Room>();
Particle<Long, Double> particle = new Particle<Long, Double>();
System.out.println(Arrays.toString(tableList.getClass().getTypeParameters()));
System.out.println(Arrays.toString(maps.getClass().getTypeParameters()));
System.out.println(Arrays.toString(house.getClass().getTypeParameters()));
System.out.println(Arrays.toString(particle.getClass().getTypeParameters()));
/**
[E]
[K, V]
[Q]
[POSITION, MOMENTUM]
 */

上面的代码里,我们想在运行时获取类的类型参数,但是我们看到返回的都是“形参”。在运行期我们是获取不到任何已经声明的类型信息的。

注意:编译器虽然会在编译过程中移除参数的类型信息,但是会保证类或方法内部参数类型的一致性。

泛型参数将会被擦除到它的第一个边界(边界可以有多个,重用 extends 关键字,通过它能给与参数类型添加一个边界)。编译器事实上会把类型参数替换为它的第一个边界的类型。如果没有指明边界,那么类型参数将被擦除到Object。下面的例子中,可以把泛型参数T当作HasF类型来使用。

public interface HasF {
    void f();
}

public class Manipulator<T extends HasF> {
    T obj;
    public T getObj() {
        return obj;
    }
    public void setObj(T obj) {
        this.obj = obj;
    }
}

extend关键字后后面的类型信息决定了泛型参数能保留的信息。Java类型擦除只会擦除到HasF类型。

Java泛型擦除的原理

我们通过例子来看一下,先看一个非泛型的版本:

// SimpleHolder.java
public class SimpleHolder {
    private Object obj;
    public Object getObj() {
        return obj;
    }
    public void setObj(Object obj) {
        this.obj = obj;
    }
    public static void main(String[] args) {
        SimpleHolder holder = new SimpleHolder();
        holder.setObj("Item");
        String s = (String) holder.getObj();
    }
}
// SimpleHolder.class
public class SimpleHolder {
  public SimpleHolder();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public java.lang.Object getObj();
    Code:
       0: aload_0
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn

  public void setObj(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class SimpleHolder
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String Item
      11: invokevirtual #6                  // Method setObj:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #7                  // Method getObj:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2
      22: return
}

下面我们给出一个泛型的版本,从字节码的角度来看看:

//GenericHolder.java
public class GenericHolder<T> {
    T obj;
    public T getObj() {
        return obj;
    }
    public void setObj(T obj) {
        this.obj = obj;
    }
    public static void main(String[] args) {
        GenericHolder<String> holder = new GenericHolder<>();
        holder.setObj("Item");
        String s = holder.getObj();
    }
}

//GenericHolder.class
public class GenericHolder<T> {
  T obj;

  public GenericHolder();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public T getObj();
    Code:
       0: aload_0
       1: getfield      #2                  // Field obj:Ljava/lang/Object;
       4: areturn

  public void setObj(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field obj:Ljava/lang/Object;
       5: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class GenericHolder
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String Item
      11: invokevirtual #6                  // Method setObj:(Ljava/lang/Object;)V
      14: aload_1
      15: invokevirtual #7                  // Method getObj:()Ljava/lang/Object;
      18: checkcast     #8                  // class java/lang/String
      21: astore_2
      22: return
}

在编译过程中,类型变量的信息是能拿到的。所以,set方法在编译器可以做类型检查,非法类型不能通过编译。但是对于get方法,由于擦除机制,运行时的实际引用类型为Object类型。为了“还原”返回结果的类型,编译器在get之后添加了类型转换。所以,在GenericHolder.class文件main方法主体第18行有一处类型转换的逻辑。它是编译器自动帮我们加进去的。 所以在泛型类对象读取和写入的位置为我们做了处理,为代码添加约束。

Java泛型擦除的缺陷及补救措施

泛型类型不能显式地运用在运行时类型的操作当中,例如:转型、instanceof 和 new。因为在运行时,所有参数的类型信息都丢失了。类似下面的代码都是无法通过编译的:

public class Erased<T> {
    private final int SIZE = 100;
    public static void f(Object arg) {
        //编译不通过
        if (arg instanceof T) {
        }
        //编译不通过
        T var = new T();
        //编译不通过
        T[] array = new T[SIZE];
        //编译不通过
        T[] array = (T) new Object[SIZE];
    }
}

那我们有什么办法来补救呢?下面介绍几种方法来一一解决上面出现的问题。

类型判断问题

我们可以通过下面的代码来解决泛型的类型信息由于擦除无法进行类型判断的问题:

/**
 * 泛型类型判断封装类
 * @param <T>
 */
class GenericType<T>{
    Class<?> classType;

    public GenericType(Class<?> type) {
        classType=type;
    }

    public boolean isInstance(Object object) {
        return classType.isInstance(object);
    }
}

在main方法我们可以这样调用:

GenericType<A> genericType=new GenericType<>(A.class);
System.out.println("------------");
System.out.println(genericType.isInstance(new A()));
System.out.println(genericType.isInstance(new B()));

我们通过记录类型参数的Class对象,然后通过这个Class对象进行类型判断。

创建类型实例

泛型代码中不能new T()的原因有两个,一是因为擦除,不能确定类型;而是无法确定T是否包含无参构造函数。

为了避免这两个问题,我们使用显式的工厂模式:

/**
 * 使用工厂方法来创建实例
 *
 * @param <T>
 */
interface Factory<T>{
    T create();
}

class Creater<T>{
    T instance;
    public <F extends Factory<T>> T newInstance(F f) {
        instance=f.create();
        return instance;
    }
}

class IntegerFactory implements Factory<Integer>{
    @Override
    public Integer create() {
        Integer integer=new Integer(9);
        return integer;
    }
}

我们通过工厂模式+泛型方法来创建实例对象,上面代码中我们创建了一个IntegerFactory工厂,用来创建Integer实例,以后代码有变动的话,我们可以添加新的工厂类型即可。

调用代码如下:

Creater<Integer> creater=new Creater<>();
System.out.println(creater.newInstance(new IntegerFactory()));

创建泛型数组

一般不建议创建泛型数组。尽量使用ArrayList来代替泛型数组。但是在这里还是给出一种创建泛型数组的方法。

public class GenericArrayWithTypeToken<T> {
    private T[] array;

    @SuppressWarnings("unchecked")
    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[] rep() {
        return array;
    }

    public static void main(String[] args) {

    }
}

这里我们使用的还是传参数类型,利用类型的newInstance方法创建实例的方式。

本文分享自微信公众号 - 喘口仙氣(gh_db8538619cdd),作者:Anymarvel

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-07-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Android之Https初识

    如果读者对上面任何一个名词陌生,请参考这篇文章: https://foofish.net/https-story-1.html

    Anymarvel
  • Android内存管理(JVM 、DVM(dalvik) 、ART简单介绍)

    本文不对JVM 、DVM(dalvik) 、ART这三者做具体的分析。只是从内存管理的角度来介绍下三者的区别和联系。

    Anymarvel
  • Git常用命令总结

    平常我们开发就是拷贝远程仓库中的一个分支,基于该分支进行开发。在开发过程中就是对工作区的操作。

    Anymarvel
  • 事件模型

     事件模型如何理解呢,举个例子,你老婆出门了,让你在家看孩子,难道你每过一分钟就去看一次孩子吗,对于计算机来说,我做一个按钮,难道就一直监听这个按钮使用没有...

    mathor
  • Spring中的Cache

    在其父类AdviceModeImportSelector的selectImports方法中,最终会回调子类的selectImports方法

    spilledyear
  • 设计模式-抽象类,只是想为你做更多

    如果说面向对象中的接口是把所有的事情扔给你的话,那么抽象类显然是想要为你做一些事情,如果实在有一部分做不了再扔给你。 相信对于大部分业务开发的场景下都不太会需要...

    ImportSource
  • Android MVP 构架初试

    目前讨论MVP MVVM 的架构也来越多,这种构架也很适合Android。研究MVP记录如下

    Javen
  • Android框架之Volley与Glide

    cMusketeer
  • 浅谈mybatis的日志适配模式 顶

    Java开发中经常用到的日志框架有很多,Log4j、Log4j2、slf4j等等,Mybatis定义了一套统一的日志接口供上层使用,并为上述常用的日志框架提供了...

    算法之名
  • 【JAVA】多线程学习2模拟一个银行存款的程序

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY 版权协议,转载请附上原文出处链接和本声明。

    韩旭051

扫码关注云+社区

领取腾讯云代金券