Java泛型之擦除

这篇博文主要记录学习Java编程思想的一些心得和体会。在这篇文中可能会引用一些优秀博文的内容,我会在文章末尾注明引用博文的地址。

演示擦除的存在

通过Java编程思想一书中的例子来对Java的泛型擦除做一个存在性的演示:

public class ErasedTypeEquivalence{
    public static void main(String[] args){
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1==c2);
    }
}
//output:
//true

通过上面的代码我们可以很容易的认为ArrayList和ArrayList是两种不同的类型。但是该程序的输出结构却令我们很意外,输出的结构却是true
其实出现该问题的原因就是Java泛型的擦除在捣鬼。

接下来,我们再来看下面的程序:

class HasF {
    public void f() {
        System.out.println("HasF f()");
    }

    public static void main(String[] args) {
        Mainpulator<HasF> hf = new Mainpulator<>(new HasF());
        hf.mainpulate();
    }
}

class Mainpulator<T>{
    private T obj;
    public Mainpulator(T x) {
        this.obj = x;
    }

    public void mainpulate() {
        obj.f();//error
    }
}

不用考虑,到你开始这样写程序的时候,你会发现写到obj.f()的时候,程序就已经报错了。报错提示:The method f() is undefined for the type T,也就是对于T这种类型f方法没有定义。那么我们来看看,T这种类型到底有写什么方法呢?
我们会发现T类型的obj展示出所有的方法分别为:

  • notifyAll()
  • equals(Object)
  • hashcode()
  • toString()
  • getClass()
  • notify()
  • wait()
    通过我列出了关于T类型的obj所有的方法后,我相信大家也知道这些方法完全就是Object对象中的方法。那么,问题由来了,T类型的obj怎么拥有的方法全都是Object类型的方法呢?
    因为,编译器编译的时候,会把泛型参数擦除到他的第一边界,(如果没有定义边界,编译器会默认为Object作为第一边界),然后把类型参数统一替换为第一边界类型。也就是说会把上面程序中的给擦除掉,关于T声明的类型参数将用Object来进行替换

已经说明上面程序报错不能运行的原因了,那么该如何进行修改上面的程序呢让其能争取的执行呢?

方式1:既然编译器把类型参数擦除到第一边界类型,我们可以对上面的程序进行向下转型。

class HasF {
    public void f() {
        System.out.println("HasF f()");
    }

    public static void main(String[] args) {
        Mainpulator<HasF> hf = new Mainpulator<>(new HasF());
        hf.mainpulate();
    }
}

class Mainpulator<T >{
    private T obj;
    public Mainpulator(T x) {
        this.obj = x;
    }

    public void mainpulate() {
        ((HasF)obj).f();//向下转型
    }
}
//output:
//HasF f()

方式2:对泛型类型参数声明边界

class HasF {
    public void f() {
        System.out.println("HasF f()");
    }

    public static void main(String[] args) {
        Mainpulator<HasF> hf = new Mainpulator<>(new HasF());
        hf.mainpulate();
    }
}

class Mainpulator<T extends HasF>{//通过extends声明T的边界为HasF
    private T obj;
    public Mainpulator(T x) {
        this.obj = x;
    }

    public void mainpulate() {
        obj.f();
    }
}
//output:
//HasF f()

总结:泛型在编译时期会被编译器进行擦除操作,并且每次擦除到泛型的第一边界处,所以java中的泛型都是伴随在边界周围发生的。

泛型擦除带来的问题

由于泛型在编译时期会被擦除,也就是说任何在运行时期需要确切类型信息的操作都将无法工作
例如下面的代码:

public class Erased<T> {

    public static final int SIZE = 100;

    @SuppressWarnings("unchecked")
    public void f(Object obj) {
        if(obj instanceof T) {}
        /**error:
         * Cannot perform instanceof check against type 
         * parameter T. Use its erasure Object instead since further generic type 
         * information will be erased at runtime
         */
        T t = new T(); //error: Cannot instantiate the type T

        T[] array = new T[SIZE];//error: Cannot create a generic array of T

        T[] obj1 = (T[]) new Object[SIZE];
    }

}

1.对于第一个报错解决的方法可以通过引用显示的Class对象来擦除进行弥补

public class ClassTypeComputer<T> {
    private Class<T> kind;

    public ClassTypeComputer(Class<T> kind) {
        this.kind = kind;
    }

    public boolean f(Object org) {
        return kind.isInstance(org);
    }

    public static void main(String[] args) {
        ClassTypeComputer<House> house = new ClassTypeComputer<>(House.class);
        System.out.println("House intanceof House:"+house.f(new House()));
        System.out.println("Mirr intanceof House:"+house.f(new Mirr()));
        ClassTypeComputer<Mirr> mirr = new ClassTypeComputer<>(Mirr.class);
        System.out.println("House intanceof Mirr:"+mirr.f(new House()));
        System.out.println("Mirr intanceof Mirr:"+mirr.f(new Mirr()));
    }
}
//output:
//House intanceof House:true
//Mirr intanceof House:true
//House intanceof Mirr:false
//Mirr intanceof Mirr:true

该程序通过显示的Class对象引用来逃避直接对确切类型信息的操作。由于泛型参数在编译的时候会被编译器擦除。因此,在运行时期需要确切类型的信息的操作将失效。

2.对于第二个报错原因除了擦除外,还有可能有一个原因,就是编译器不能确定T类型是否有默认类型的构造器。解决此方式可以显示的引用工厂类来创建类型实例

public class ClassFactory<T> {
    private Class<T> kind;

    public ClassFactory(Class<T> kind) {
        this.kind = kind;
    }

    public T create() {
        try {
            return kind.newInstance();
        }catch(Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        ClassFactory<Student> s = new ClassFactory<>(Student.class);
        System.out.println(s.create());
    }
}

class Student{}
//output:
//generics.Student@2401f4c3

前面两个说了泛型擦除对判断类型和创建new实例带来的问题。现在我们来了解一下泛型擦除对数组带来的一些问题:
3.泛型擦除在数组中的问题:

class Generic<T>{}

public class ArrayOfGerneric {
    static final int SIZE = 100;
    static Generic<Integer>[] gia;


    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        //下面这段程序在运行时期会报ClassCastException异常
        gia = (Generic<Integer>[]) new Object[SIZE];

        gia = new Generic[SIZE];
        System.out.println(gia);
        gia[0] = new Generic<Integer>();
        //下面两句代码都是绘制编译时期报错
        //gia[1] = new Object();
        //gia[2] = new Generic<Double>();
    }
}

当然,大家看到这个程序的时候会发现,这个数组只有后面两行的代码才跟泛型有一定的关系。对于main方法中的第一行代码,为什么会在运行时期报错呢?
因为在编译时期把数Object[]换为Generic[]类型的数组了,但是数组有一个特性:数组的实际类型时在创建数组的时候才会确定。也就是说在编译时期的转换信息只存在编译时期,而在运行时期他仍然是Object[]数组。当然也可以说数组是不能进行转型的。

接下来我们再分析下面的代码:

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

    @SuppressWarnings("unchecked")
    public ArrayGeneric(int size) {
        array = (T[]) new Object[size];
    }

    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) {
        ArrayGeneric<Integer> gai = new ArrayGeneric<Integer>(100);
        System.out.println(gai.array);
        //Integer[] i = gai.rep();//error:ClassCastException
    }

}
//output:
//[Ljava.lang.Object;@4361bd48
//2

通过上面的代码,我们可以看出来,当运行此程序的时候,能够顺利的创建数组,但是通过打印信息,我们可以看出来,创建的数组却是Object类型的数组,而并不是Integer类型的数组。并且如果我们如果执行rep方法,会报ClassCastException异常。出现这些原因究竟是什么原因呢?
其实,我们已经说了,Java中的泛型会在编译时期进行擦除操作。因此类型参数,会被Object所取代,所以就能正确的创建出Object类型的数组,但是当我们执行rep方法的时候,会把Object类型的数组返回给Integer类型的数组,所以就会报ClassCastException异常。如果对上面的程序进行修改,让泛型参数的边界不再是Object类型,在创建数组的时候就会报ClassCastException异常。请看代码:

public class ArrayGeneric<T extends Integer> {
    private T[] array;

    @SuppressWarnings("unchecked")
    public ArrayGeneric(int size) {
        array = (T[]) new Object[size];
    }

    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) {
        ArrayGeneric<Integer> gai = new ArrayGeneric<Integer>(100);
    }

}//output:
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
    at generics.ArrayGeneric.<init>(ArrayGeneric.java:8)
    at generics.ArrayGeneric.main(ArrayGeneric.java:24)

大家可以看出来这段代码和上面的代码就只是在声明泛型参数的时候有所不同,这段代码通过限定泛型参数T的第一边界是Integer类型。如果这样的话,我们在创建数组的时候,就相当于我们在创建数组的时候,对数组进行了(Integer[])的转换。因此在创建数组的时候会报ClassCastException异常导致创建失败。
为了更好的理解这两点的不同,我分别给出上面两段代码构造器的反编译代码进行对比:

  • ArrayGeneric
public class generics.ArrayGeneric<T> {
  public generics.ArrayGeneric(int);
    Code:
       0: aload_0
       1: invokespecial #12                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iload_1
       6: anewarray     #3                  // class java/lang/Object
       9: putfield      #15                 // Field array:[Ljava/lang/Object;
      12: return
  • ArrayGeneric
public class generics.ArrayGeneric<T extends java.lang.Integer> {
  public generics.ArrayGeneric(int);
    Code:
       0: aload_0
       1: invokespecial #12                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iload_1
       6: anewarray     #3                  // class java/lang/Object
       9: checkcast     #15                 // class "[Ljava/lang/Integer;"
      12: putfield      #16                 // Field array:[Ljava/lang/Integer;
      15: return

注意看这代码,在第二段代码中,程序对创建的Object数组进行了checkcast转型为Integer类型的操作,因此这就是程序出错的原因。

通过这些代码,我们可以发现,泛型并不是很适用在数组中。那么我们非要适用泛型数组该怎么办呢?

  • 方式1:适用ArrayList容器类代替数组,其底层也是数组的实现。
  • 方式2:我们可以根据Array.newInstance方法去创建对应类型的数组(推荐)。
public class ArrayGeneric2<T> {
    private T[] array;

    @SuppressWarnings("unchecked")
    public ArrayGeneric2(Class<T> cls,Integer size) {
        array = (T[]) Array.newInstance(cls, size);
    }

    public void put(T item,Integer index) {
        array[index] = item;
    }

    public T get(Integer index) {
        return array[index];
    }

    public T[] rep() {
        return array;
    }

    public static void main(String[] args) {
        ArrayGeneric2<Integer> a = new ArrayGeneric2<>(Integer.class,10);
        System.out.println(a.array);//[Ljava.lang.Integer;@53bd815b
        Integer[] rep = a.rep();
        ArrayGeneric2<String> a2 = new ArrayGeneric2<>(String.class,10);
        System.out.println(a2);//[Ljava.lang.String;@53bd815b
        String[] rep2 = a2.rep();
    }

}

同样给出这种方式构造的反编译代码:

public class generics.ArrayGeneric2<T> {
  public generics.ArrayGeneric2(java.lang.Class<T>, java.lang.Integer);
    Code:
       0: aload_0
       1: invokespecial #13                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: aload_1
       6: aload_2
       7: invokevirtual #16                 // Method java/lang/Integer.intValue:()I自动拆箱
      10: invokestatic  #22                 // Method java/lang/reflect/Array.newInstance:(Ljava/lang/Class;I)Ljava/lang/Object;
      13: checkcast     #28                 // class "[Ljava/lang/Object;"转型
      16: putfield      #29                 // Field array:[Ljava/lang/Object;
      19: return

如果我们此时同时也给泛型参数限定边界:

public class ArrayGeneric2<T extends Integer> {
    private T[] array;

    @SuppressWarnings("unchecked")
    public ArrayGeneric2(Class<T> cls,Integer size) {
        array = (T[]) Array.newInstance(cls, size);
    }

    public void put(T item,Integer index) {
        array[index] = item;
    }

    public T get(Integer index) {
        return array[index];
    }

    public T[] rep() {
        return array;
    }

    public static void main(String[] args) {
        ArrayGeneric2<Integer> a = new ArrayGeneric2<>(Integer.class,10);
        System.out.println(a.array);//[Ljava.lang.Integer;@53bd815b
        Integer[] rep = a.rep();
    }

}

运行代码任然不会报错。这里也给出构造器的反编译代码:

public class generics.ArrayGeneric2<T extends java.lang.Integer> {
  public generics.ArrayGeneric2(java.lang.Class<T>, java.lang.Integer);
    Code:
       0: aload_0
       1: invokespecial #13                 // Method java/lang/Object."<init>":()V
       4: aload_0
       5: aload_1
       6: aload_2
       7: invokevirtual #16                 // Method java/lang/Integer.intValue:()I自动拆箱
      10: invokestatic  #22                 // Method java/lang/reflect/Array.newInstance:(Ljava/lang/Class;I)Ljava/lang/Object;
      13: checkcast     #28                 // class "[Ljava/lang/Integer;"将newInstance返回的Object对象转换为Integer数组
      16: putfield      #29                 // Field array:[Ljava/lang/Integer;
      19: return

Java泛型擦除的原因

关于Java泛型出现的原因,我这里就简单的说一下:由于,Java从1.0的时候并不支持泛型。而是在Java5的时候才支持的。所以,考虑到版本的兼容性问题和一种折中的选择,才使Java泛型在编译时期被擦除,而在运行时期,Java中是没有泛型的。

如果我理解的有误,欢迎大家给我指出来,我下来再去改正,让我们一起进步吧!

目前这篇博客主要是来自对Java编程思想的一些学校领悟吧!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值