java范型类型擦除

一. 概述

  在上一篇范型博客中,主要讲解了范型的基本使用和设计,在本篇博客中主要讲解使用范型带来的问题和解决方法。
  在使用java范型时,我们通常会遇到诸多问题,例如不能使用T.class,不能直接使用T t.method(),不能通过范型参数实现方法重载等等,这一切的原因都归结于-“范型类型擦除”。

二.什么是类型擦除

  我们知道,jdk1.5以前是不支持范型的,以List为例,在List进行add()时是不进行类型检查的,所以在编译期不论存入什么类型的对象都不会编译出错,但是在运行时,一旦检测到不一样的类型就会运行出错。当范型出现以后,在编译阶段就会检查存入对象的类型,一旦存入不一样的类型,在编译阶段就会报错。所以说范型是一种把运行期异常转化到编译期异常的一种手段。正因如此,范型仅仅在我们写源码时帮我们进行一些类型检查,类型转化等工作,当编译结束后在运行期是没有任何范型语法的,这个把源码中去除范型的过程就是范型的类型擦除。

三.为什么要使用范型类型擦除

  其实说起来很简单,因为到目前为止,任何版本的jvm在运行阶段都不支持范型,但是范型语法在实际开发中是肯定需要的,很多其他语言都支持范型(例如C++的模板,python接用anything来声明一个perform泛型方法等等)java语言不能落后啊,但是又由于历史原因,Java在版本1.0中是不支持泛型的,这就导致了很大一批原有类库是在不支持泛型的Java版本上创建的。如果后期要加入范型,就必须使得原有的非泛化类库能够在泛化的客户端使用,所以Java开发者使用了一个折中的办法,就是擦除,具体擦除过程请看如下代码。

class User<T extends Fruit> {
     public T work(T t){
         return null;
     }
}
class Fruit{}

class Apple extends Fruit{}

class Pear extends Fruit{}
   类型擦除后如下:
class User {
     public Fruit work(Fruit t){
         return null;
     }
}

  我们看到,范型的语被移除,同时,范型的类型被限定为最左边界(最上级父类),那么范型的类型擦除过程可以总结如下:
  1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
  2.移除所有的类型参数。
  通过范型擦除,虽然语法没有了范型,但是在运行阶段依然可以将传入类型限定为Fruit的子类,这就是类型擦除存在的意义,但是如果仅仅是这么简单的擦除,依然存在一些问题,请看如下代码:

public interface Comparable<T> {
    public int compareTo(T o);
}

class User implements Comparable<User> {
    @Override
    public int compareTo(User o) {
        return 0;
    }
}
类型擦除后如下:
public interface Comparable {
    public int compareTo(Object o);
}
class User implements Comparable {
    public int compareTo(Object o) {
        return compareTo((User)o);
    }
    public int compareTo(User o){
    }
}
   类型擦除后,Comparable接口的范型被擦除,compareTo方法中变成Object类型,但是User的compareTo方法却是User类型,这时就导致Overwrite失败,这时编译器增加了一个Object类型的桥接方法,依然保证编译成功。

四.范型类型擦除带来的问题及解决办法

1. 无法获取范型的class
class User{
    public void eat(){
        this.printClass(T.class);//此处编译出错,因为类型擦除,这里的T其实是不存在的。
    }
    private void printClass(Class clazz){
        System.out.println(clazz);
    }
}

  为了解决这个问题,我们常用的做法就是使用RTTI,即Run-Time Type Identification,通过运行时类型识别,具体代码如下:

class User{
    private Class clazz;

    public User(Class clazz){
        this.clazz = clazz;
    }

    public void eat(){
        this.printClass(clazz);
    }

    private void printClass(Class clazz){
        System.out.println(clazz);
    }

    public static void main(String[] args){
        User u = new User(Fruit.class);
        u.eat();
    }
}

  我们在实例化User的时候,必须把范型的class也传入进去,这样可以解决,其实很多范型dao设计就是这种思路。

2. 无法直接调用范型方法
class User{
    public void eat(T t){
       t.weight();//这里编译出错,因为类型擦除,此时T是Object不可能有weight方法,
    }
}

  这里也只能用一个折中方案,我们可以将T限定为某个类的子类,这样可以调用父类的方法,代码如下:

class User<T extends Fruit>{
    public void eat(T t){
       t.weight();
    }
}

  上面代码就不会出错,虽然类型擦除,但是类型会被限定为Fruit,fruit是拥有weight方法的。

3. 无法通过范型来进行方法重载
class User{
    // 方法编译不通过,因为类型擦除,其实传入的List都是无范型的,所以入参其实都是List类型
    public void eat(List list){}

    public void eat(List list){}

}

  因为这个类的eat方法,应该是期望即能传入List,也能传入List,想解决这个问题,就使用通配符,通配符在上一篇博客也有介绍,不了解的可以去查看。代码如下:

class User{

    public void eat(List extends Fruit> list){

    }

    public static void main(String [] args){
        User u = new User();
        List applist = new ArrayList();
        u.eat(applist);

        List pearList = new ArrayList();
        u.eat(pearList);
    }
}

  通过通配符的方式,解决了上述问题

4.泛型类的静态变量是共享的
public class StaticTest{
    public static void main(String[] args){
        User gti = new User();
        gti.var=1;
        User gts = new User();
        gts.var=2;
        System.out.println(gti.var);
    }
}
class User{
    public static int var=0;
    public void nothing(T x){}
}

  输出结果——2!由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的。,当然类型擦除带来的问题还有很多,这里不一一列举。

五.总结

  范型是java编码尤其是设计中常用到的语法,要想解决设计过程中遇到的问题,就必须深入了解jvm处理范型的过程,希望本片博客对大家有用。

你可能感兴趣的:(core,java)