likes
comments
collection
share

不控制了,向Java泛型开炮!

作者站长头像
站长
· 阅读数 6

Java中的泛型作为一个基本特性,但其中还是会有一些一时间让人捉摸不透的概念。

本文或能帮你解除以下疑惑。

  • 泛型擦除怎么回事?
  • 泛型类的继承关系是怎么被破坏的?
  • PECS原则是怎么回事?
  • 泛型擦除如何获取泛型的的类型?

1)概念

1.1) 简单理解泛型概念

泛型:参数化的类型,也可以理解为类型的参数化。对比着理解,java中我们定义一个方法的时候,会定义其方法参数、方法返回值、方法名、访问控制符、方法体等。比如

public  String fetchLabel (int number){ return count + ""; }//code_1
fetchLabel(1)//code_2

其中 ,code_1处 int number 是方法参数,定义了一个名为number类型为int的参数也就是方法形参。 code_2处调用方法传入具体的参数1,此时传入的这个实际参数1就是方法的实参。类似地,把原来具体的类型指定的确切类型进行参数化处理,如下代码Code_3处原来Response类的属性data类型为确切的类型Student类型,编写为一个参数化类型类似于方法的形参 T。在使用时传入具体的类型Student,类似方法的实参。这样就实现了类型的参数化

class Response  { Student data; }//code_3
class Respons<T>{ T data;       }//code_4

1.2)相关术语

为方面描述和进一步加深对泛型相关概念的理解,下面以List为例来说明一下跟泛型相关的术语

  • List :原生类型(Raw Type)
  • List<E>List<T extends Number> 泛型 (Generic Type)
    • 其中E为形式类型参数
    • <T extends Numbser>为有限制类型参数
  • List<Integer>List<?>List<? exetends Number>参数化的类型 (Parameterized Type)
    • 其中Integer为实际参数类型也可叫类型实参
    • ?无限统配类型 - <? extends Numbser> 为有限通匹配类型

以上是对泛型这一概念的通俗理解,至于平时大家惯常的使用这里就不赘述了。但很多情况下我们使用泛型时会遇到一些奇奇怪怪的有反直觉的问题,我们详细的聊下。

2)消失的类型

//code_5
Response<Student> studentResponse = new Response<>()
Response<Apple> appleResponse = new Response<>();

Class stuClz   = studentResponse.getClass()
Class appleClz = appleResponse.getClass()
System.out.println(stuclz == appleClz) //true  code_6

如上code_5代码最终code_6为true。咦!泛型类型消失了

Java语言泛型的引入提供了严格的编译检查,但是在编译器会有类型参擦除的特性,同时编译器会对泛型进行类型擦除主要体现的三个方面:

  • 泛型类和泛型接口类型擦除
  • 泛型方法类型擦除
  • 生成桥接方法

2.1) 泛型类和泛型接口类型擦除

参数化类中的实际类型会被擦除。用边界类型或者Object替换实际类型参数 。在运行的时候泛型信息是不保留的。例如:

  class Box<T> {
      T item;
      public void setItem(T item) {
          this.item = item;
      }
  }
  //泛型擦除后
    class Box {
      Object item;
      public void setItem(Object item) {
          this.item = item;
      }
  }
  

2.2) 泛型方法类型擦除

泛型方法中的实际类型参数会被擦除

//代码
public static <T> printProductInfo(T product){
  System.out.println(e.toString())
}

//编译中类型擦除后
public static printProductInfo(Object product){
  System.out.println(product.toString())
}

2.3 ) 泛型擦除影响以及桥接方法

自动生成桥接方法,由于泛型擦除的特性会出现以下情况

class Box<T> {
    T item;
    public void setItem(T item) {
        this.item = item;
    }
}

class  MyBox extends Box<Integer>{ 
    @Override
    public void setItem(Integer item) {
        super.setItem(item);
    }
}
//类型擦除后
class Box {
     Object item;
    public void setItem(Object item) {
        this.item = item;
    }
}

class  MyBox extends Box<Integer>{
    Object t;
    @Override
    public void setItem(Integer item) {
        super.setItem(item);
    }
}

MyBox作为Box的子类,setData()方法的方法签名出现了子类和父类不一致的情况,也就是说方法复写失效了,为了解决这个问题保护java多态性,Java编译器引入了Bridge Method。会在泛型的子类中生成Bridge Method。这样子类和父类的setItem()方法就有了同一签名。

 class  MyBox extends Box<Integer>{
      Object t;
      public void setItem(Object item) {//bridge method:保证与父类方法有相同的签名,保护Java多态性
          setItem((Integer)item);
      }
      public void setItem(Integer item) {
      }
  }

3) 反直觉的问题

不控制了,向Java泛型开炮!

3.1)、List<Integer>List<Number>的子类型吗?

  • 虽然,Integer是Number是子类,但List<Integer>List<Number>并不是子父关系。意味着如下代码是编译不通过的
public void containerOpt(List<Number> numbserList){ /* todo */}
containerOpt(new ArrayList<Integer>())
  • 另外泛型有子类化规则,所谓子类化规则List<Integer>为原生态类型List的子类,但不是List<Object>的子类。下面的代码虽然编译检查能通过但容易产生运行期的异常,Java1.5之后引入泛型概念,不建议使用裸奔的原生类型。

    List rawList = new ArrayList<String>()//编译检查不会报错
    
  • 凡事都有类外,有时候我们不得不使用原生类型,主要在两种情况下

    • 当我们需要取Class时,比如List.class是没有问题。但是List<String>.class是不允许的
    • 当我们使用instanceIOf判断类型的时候

3.2) 他们 List, List<?>, List<Object> 有啥区别?

  • List<?>类型参数类型,泛型类型声明,可以引用任何类型的List。比如List<String>,List<Integer>,List<Object>。通过这个声明引用,我们可以调用其get()方法获取里面的元素。等价于List<? extends Object>
  • List可以看做一个普通的Raw原始类型
  • List<Object>,Object这一特定类型的泛型类型的泛型类型。

看下面这段代码,思考下他们的区别

List rawList = new ArrayList<String>();//√
rawList = new ArrayList<Integer>();//√
rawList = new ArrayList<Object>();//√
rawList.get(0);//√
rawList.add(new Object());//√

List<Object> objList = new ArrayList<String>();// ×
objList = new ArrayList<Object>();// √
objList = new ArrayList<>();// √
objList = new ArrayList<String>(); //×

List<?> genericList = new ArrayList<Object>(); //√

genericList = new ArrayList<String>(); //√
genericList = new ArrayList<Integer>();//√
Object e = genericList.get(0);//√
genericList.add(new String("x"))//×

4)解决之道-驾驭它

4.1) 泛型统配通配符、带边界的通配符 PECS规则

不控制了,向Java泛型开炮!

4.1.1) 上边界通配符 ? extends T

  1. List<? extends T> 含义是集合里的对象类型是T的子类。
  2. 基于第一点,我们可以往此集合取出对象,作为T类型。无论是T类或者其子类,都可被当做T类型。
  3. 集合里的对象的类型是一个有边界的范围T或T的子类,父类引用子类对象,子类不能直接引用父类对象 故不能往上边界符中的容器中添加对象。

4.1.2)下边界通配符 ? super T

  1. List<? super T> 表示List里面的元素是T的超类,但不知道是具体哪一种超类。
  2. 基于这一点,可以往集合里添加T以及T子类的对象。因为T以及T子类都可以被当做T以及T的超类类型
  3. 集合里的对象的类型是一个有边界的范围T或T的父类不能取元素,不确认容器内是具体哪个超类。能确认T类型一定是容器中类型的子辈。

4.1.3)PECS

所谓的PECS(Producer Extends Consumer Super)其实就是如上我们描述的这样的规则

  • 当我们使用上边界符 ? extends T 时,我们只能保证这个容器中的对象都继承自T或者是T类本身。因而我们可以在集合中取出的数据都当做T这种类型,此时这个容器对外提供对象,充当了生产者Producer的角色,故称之为【PE】 Producer Extends ;
  • 当我们使用下边界符? super T时,能保证容器中的对象都为T的父类。因而我们可以往容器中放T或者T的子类。此时容器是向内吸收对象的,充当消费者Consumer 的角色故称之为【CS】Consumer Super。

具体的时候可以参考上面的代码。

//方法1:
private static void producer(List<? extends Exception> exceptions) {
    Exception exception = exceptions.get(0);
    Exception exception1 = exceptions.get(1);
}

//方法2:
private static void consumer(List<? super RuntimeException> runtimeList) {
    runtimeList.add(new IllegalArgumentException());
    runtimeList.add(new RuntimeException());
    runtimeList.addAll(new ArrayList<RuntimeException>());
}

//方法3:
private void doSomething() {
     //1.声明引用
     List<? extends RuntimeException> runtimeExceptionList = new ArrayList<IllegalArgumentException>();
     List<? super Exception> exceptionList = new ArrayList<Throwable>();
	
    //2.传参
     //2.1 生产的超类
     producer(new ArrayList<RuntimeException>());
     producer(new ArrayList<IllegalArgumentException>());
     producer(new ArrayList<Exception>());


     //2.2 消费超类
     consumer(exceptionList);
     consumer(new ArrayList<Throwable>());
 }

4.2) 挣脱类型擦除

  • 如第2章节所述,运行期的泛型类型就会被擦除,如List运行期类型被擦除成了List。我们进行JSON字符串转对象数据反序列化的时候需要知道准确的类型。emm...真不知道宇宙和烦恼哪个会先老去

  • 回想使用Gson这种第三方库的时候反序列化的时候,我们能够通过TypeToken能够获取泛型类的准确类型,实现数据的反序列化。

  • 秘密就在于泛型类的子类能通过getGenericSuperclass()拿到其父类的参数化类型,通过 getGenericSuperclass//返回父类的Type,这时候他已经是一个ParameterizedType了,从而获取到真是的参数。虽然类型擦除了但在字节码里能找到它的真正的类型。 具体可以参照 stackoverflow.com/questions/9…

  • 另外值得注意的是我们在使用TypeToken的时候,需要实现一个子类我们通常会new TypeToken(){}这其实是实现了一个继承了TypeToken匿名类的对象,这样TypeToken这个匿名对象内部可以通过getGenericSuperclass()获取到父类的具体类信息。

  • 5) 结

    本文从Java泛型的概念到特点到一些问题解决方案,阐述了Java泛型相关的问题。

    不足之处,不吝批评指正。

    参考: