你真的了解java中的泛型吗?

你真的了解java中的泛型吗?

​ 泛型是JDK1.5中引入的重要特性,JDK集合包中存在大量泛型相关的代码。但你真的了解泛型吗,你之前理解泛型是否存在一些误解呢?比如说到泛型的类型擦除。

public interface Container<K, V> {
   	V find(K key );
    void add(K key, V value);
    boolean contain(K key);
}
//编译器编译后进行类型擦除,类型参数E将变为Object
public interface Container {
   	Object find(Object key );
    void add(Object key, Object value);
    boolean contain(Object key);
}
复制代码

现在问题来了是不是类型擦除后,运行时就不能获取源代码中Container<K, V>的类型参数K与V的信息呢?如果你认为不能,那么你真的对泛型的类型擦除存在误解。通过javap -v Container.class 查看Container.class的节字码信息。下面是Container.class节字码的部分信息。

Constant pool:
	//...省略部分
	#6 = Utf8               (TK;)TV;
	//...省略部分
	#9 = Utf8               (TK;TV;)V
  //...省略部分
  #12 = Utf8               (TK;)Z
  #13 = Utf8               <K:Ljava/lang/Object;V:Ljava/lang/Object;>Ljava/lang/Object;
  //...省略部分
{
  public abstract V find(K);
    descriptor: (Ljava/lang/Object;)Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_ABSTRACT
    Signature: #6                           // (TK;)TV;

  public abstract void add(K, V);
    descriptor: (Ljava/lang/Object;Ljava/lang/Object;)V
    flags: ACC_PUBLIC, ACC_ABSTRACT
    Signature: #9                           // (TK;TV;)V

  public abstract boolean contain(K);
    descriptor: (Ljava/lang/Object;)Z
    flags: ACC_PUBLIC, ACC_ABSTRACT
    Signature: #12                          // (TK;)Z
}
Signature: #13                          // <K:Ljava/lang/Object;V:Ljava/lang/Object;>Ljava/lang/Object;
复制代码

可能看到通过类的附加Signature签名信息保留的泛型的实际类型,就是参数类型K与V,而这些信息又被保存在常量池的#13中。

#13 = Utf8               <K:Ljava/lang/Object;V:Ljava/lang/Object;>Ljava/lang/Object;
复制代码

同样方法的Container类中的方法find、add、contain的附加Signature签名信息中也分别保存了泛型的实际类型信息。可以通过下面的方式获取Container的类型参数K与V的信息。

public static void main(String args[]) {
  TypeVariable<Class<Container>>[] tvArray = Container.class.getTypeParameters();
  for (TypeVariable<Class<Container>> typeVariable : tvArray) {
    System.out.println(typeVariable.getTypeName());
  }
}
复制代码

​ 再比如泛型通配符使用时要遵守PECS原则,即要生产对象用于获取时会用 <? extends T> ,消费对象用于生成与处理时会用<? super T>,但为什么要遵守PECS原则呢?不遵守一定会出错吗?本文将深入了字节码带你了解编译器在处理泛型时到底做些什么?同时将介绍泛型的不变(invariance)、协变(covariance)、逆变(contravariance),然后结合例子深入分析为什么java泛型本身不支持协变,为什么要通过引入下界通配符来支持逆变 ;当然本文也介绍泛型相关的反射知识,如ParamerterizedType、TypeVariable、WildcardType、GenericArrayType等。

1 为什么要引入泛型

​ 泛型又称作参数化类型,既然叫参数化类型,那一定和通常理解的参数与类型有关。参数,通常理解就是方法的一个变量,定义方法时我们采用参数,而调用方法时将实际的值传入。类型,通常理解就是类class的概念。那这个参数化类型,就可以理解为将类型进行参数化,定义时并不确定他的类型是什么,再实际使用时再将要使用到的类型作为参数传入。平时方法调用传的是变量的值,而泛型使用时传入的是具体的类型。

1.1 增强编译器类型检测

​ 在回答为什么java语言中要引入泛型这个问题之前,先来看看没有泛型之前一些场景如何处理。假设要计算销售人员具体工资,销售员的工资包括基本工资、提成费用、出差费用等费用。现在定义了一个Calculator的接口,其定义了一个calculateFee方法专门用于计算费用。同时给出了基本工资、提成费用、出差费用对应的费用计算实现类分别为BaseSalaryCalculator、CommissionFeeCalculator、TravelAllowanceCalculator。

import java.math.BigDecimal;
public interface Calculator {
  //计算费用
  BigDecimal calculateFee();
}
import java.math.BigDecimal;
//基本工资计算
public class BaseSalaryCalculator implements Calculator {
  private final BigDecimal baseSalary;
  public SalaryCalculator(BigDecimal baseSalary) {
    this.baseSalary = baseSalary;
  }
  BigDecimal calculateFee() {
    //具体计算逻辑省略....
  }
}
import java.math.BigDecimal;
//提成费用计算
public class CommissionFeeCalculator implements Calculator {
  private final BigDecimal salesAmount;
  public ValueCalculator(BigDecimal salesAmount) {
    this.salesAmount = salesAmount;
  }
  BigDecimal calculateFee() {
    //具体计算逻辑省略....
  }
}
public class TravelAllowanceCalculator implements Calculator { 
  private final Integer travelDays;
  private final BigDecimal travelCost;
  public TravelAllowance(Integer travelDays, BigDecimal travelCost) {
    this.travelDays = travelDays;
    this.travelCost = travelCost;
  }
  BigDecimal calculateFee() {
    //具体计算逻辑省略....
  }
}
复制代码

​ 在计算销售员的工资时,定义了一个calculateTotalSalary方法,该方法的参数calculatorContainer是一个List,其保存着工资所包括基本工资、提成费用、出差费用等。calculateTotalSalary方法内部遍历List,取出之前存入各类的费用计算的Calculator,没有泛型时List里面只能存放Object,于是从List中获取的元素的类型也是Object,但由于要调用Calculator的calculateFee方法计算费用, 这时只能将Object强制转换成Calculator,再调用Calculator的calculateFee方法计算费用,累加后返回销售员的工资。

BigDecimal calculateTotalSalary(List calculatorContainer) {
  BigDecimal salary = BigDecimal.ZERO;
  for (int i = 0 ; i < calculatorContainer.size(); i++) {
    Calculator calculator = (Calculator) calculatorContainer.get(i);
    salary = salary.add(calculator.calculateFee());
  }
  return salary;
}
复制代码

​ 上面的calculateTotalSalary方法似乎没什么问题,但如果有谁在调用calculateTotalSalary方法时,传的参数calculatorContainer中错误的加一非Calculator类型的对象呢?这时上面的 (Calculator) calculatorContainer.get(i)这行代码在运行时将抛出ClassCastException异常。问题的关键在于这个异常只有在运行时才能感知到,在这之前这种潜在的问题根本无法知道。更加的严格的类型检查,显然应该在代码编译期间就被编译器捕获,而不应该将这个问题拖延到运行时。泛型的引入一个初衷便是加强编译期间类型的检测。有了泛型后上面的calculateTotalSalary方法便可以改造成下面这样。

BigDecimal calculateTotalSalary(List<Calculator> calculatorContainer) {
  BigDecimal salary = BigDecimal.ZERO;
  for (int i = 0 ; i < calculatorContainer.size(); i++) {
    //不会再进行强制的类型转换,编译器会插入对应的类型转换
    Calculator calculator = calculatorContainer.get(i);
    salary = salary.add(calculator.calculateFee());
  }
  return salary;
}
复制代码

此时如果想再往calculateTotalSalary的参数calculatorContainer中加入非Calculator类型的对象,编译器在编译时便会报错。

1.2 支持泛型编程复用代码

​ 另外一方面泛型的引入使得代码复用性变得更强了。如上面的Container<K, V>,在实际使用时可以为类型参数K传入String,类型参数V传入Entity。

import java.util.concurrent.ConcurrentHashMap;
public abstract AbstractContainer interface Container<String, Entity> {
  private Map<String, Entity> container = new ConcurrentHashMap();
  Entity find(Entity key) { return container.get(key);}
  void add(String key, Entity value) {container.put(key, value);}
  boolean contain(String key) { return container.containsKey(key);}
}
复制代码

这样我们不需要为特定的Container一一定义对应的某某Container接口,在具体使用时再将指定的类型传给类型参数K与V便可以。上面的那个AbstractContainer实现就是Map。JDK的集合类型,如List因为泛型的引入,处理变得更加方便。

2 编译器的类型擦除包括什么

​ java语言中引入泛型,以便支持编译时更加安全的类型的检测与泛型编程。为了实现泛型java编译器采用了类型擦除。java编译器的类型擦除实际包括三个方面泛型中类型参数的替换、必要时插入类型转换、生成桥接方法支持泛型的多态。

2.1 类型参数替换

​ java编译器在进行编译时会将泛型中的类型参数进行替换。如果类型参数指定了边界则用第一个边界替换,如果未指定边界则采用默认的父类Object进行替换。

public class Node<T> {
    private T data;
    private Node<T> next;
    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }
    public T getData() { return data; }
    // ...
}
复制代码

由于类型参数T未指定边界,编译器进行类型擦除后将T替换为Object。

public class Node {
    private Object data;
    private Node next;
    public Node(Object data, Node next) {
        this.data = data;
        this.next = next;
    }
    public Object getData() { return data; }
    // ...
}
复制代码

再看一下类型参数指定边界的例子。

public class Node<T extends Comparable<T> & Serializable> {
    private T data;
    private Node<T> next;
    public Node(T data, Node<T> next) {
        this.data = data;
        this.next = next;
    }
    public T getData() { return data; }
    // ...
}
复制代码

java编译器在编译时将类型参数T,替换为其第一个类型边界Comparable。这里类型参数T指定了上界同时为Comparable与Serializable。但在类型擦除时,会采用第一个边界Comparable进行替换。

public class Node {
    private Comparable data;
    private Node next;
    public Node(Comparable data, Node next) {
        this.data = data;
        this.next = next;
    }
    public Comparable getData() { return data; }
    // ...
}
复制代码

注意:虽然java支持泛型,但泛型实际是一种语法糖,因为JVM中根本没有对应的泛型类型,java泛型的处理过程是在编译期,而不是运行期。比如,定义了一个List 类型的实例变量list,虽然没办法直接向list中加入类型为Integer的值(编译器将报错),但依然可以通过反射向list中类型为Integer的值,只是这么做,后面在取出时会出现问题。

2.2 插入类型转换

​ java编译器在编译源码处理泛型时,在必要时会在生成的字节码中插入checkcast指令。指令checkcast用于检查类型强制转换是否可以进行。如果可以进行,那么checkcast指令不会改变操作数栈,后继的赋值操作可以进行,否则它会抛出ClassCastException异常。

import java.util.List;
import java.util.ArrayList;
public class Main {
    public static void main(String args[]) {
        List<String> list = new ArrayList<>();
        list.add("aaa");
        String s = list.get(0);
    }
}
复制代码

如果没有java编译器编译插入checkcast指令的话,根据上面介绍的类型参数替换,那得到的将得到如下的结果。

import java.util.List;
import java.util.ArrayList;
public class Main {
    public static void main(String args[]) {
        List list = new ArrayList();
        list.add("aaa");
      	//类型参数替换后list.get(0)返回应该是Object,而不是String。如果没有编译器的插入checkcast指令,这里编译将出现异常。
        String s = list.get(0);
    }
}
复制代码

先通过javac -g Main.java, 生成字节码,其中-g指定参数生成所有调试信息。然后通过javap -v Main.calss 查看Main.class的节字码信息。

//...省略部分
//...常量池信息
Constant pool:
   #1 = Methodref          #9.#29         // java/lang/Object."<init>":()V
   #2 = Class              #30            // java/util/ArrayList
   //...常量池信息
   #6 = InterfaceMethodref #32.#34        // java/util/List.get:(I)Ljava/lang/Object;
   #7 = Class              #35            // java/lang/String
   //...常量池信息
{ //...省略部分
   public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: ldc           #4                  // String aaa
        11: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        16: pop
        17: aload_1
        18: iconst_0
        19: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        24: checkcast     #7                  // class java/lang/String
        27: astore_2
        28: return
      LineNumberTable:
        line 5: 0
        line 6: 8
        line 7: 17
        line 8: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  args   [Ljava/lang/String;
            8      21     1  list   Ljava/util/List;
           28       1     2     s   Ljava/lang/String;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8      21     1  list   Ljava/util/List<Ljava/lang/String;>;
  //...省略部分 
}            
复制代码

LineNumberTable中记录了源代码代码开始行数到字节码Code 中JVM指令开始行数的对应关系,Main源码的第7行String s = list.get(0);对应JVM指令的第17行。也就是String s = list.get(0); 被折分成了下面的JVM指令

17: aload_1 18: iconst_0 19: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object; 24: checkcast #7 // class java/lang/String 27: astore_2

对应的JVM指令的含义如下:

17: aload_1 // 将引用类型变量list压入操作数栈,aload_1中的1代表LocalVariableTable中Slot为1的变量, 即list。
18: iconst_0 // 将常量整数常量0压入操作数栈,这里的0是list.get(0)对应的常量0。
19: invokeinterface #6, 2 //调用接口方法,接口方法信息存在Constant pool的#6与#2中,最后能得到,实际调用是 java/util/List.get:(I)Ljava/lang/Object; 其实就是List的get方法
24: checkcast     #7  // 检测List.get结果是否可以转换成String, 不行抛出ClassCastExecption。
27: astore_2 // 将操作数栈栈顶元素出栈,并赋给局部变量表中的s,astore_2中的2代表LocalVariableTable中Slot为2的变量,即s。
复制代码

注:

1、LocalVariableTable是局部变量表,aload_n与astore_n,中的n都是去操作局部变量表中solt为n对应的变量。

2、在调用 invokevirtual指令前,需要将对象的引用和方法参数压入操作数栈,调用结束将对象引用和方法参数会出栈,如果方法有返回值,返回值会压入操作数栈栈顶。

3、指令checkcast用于检查类型强制转换是否可以进行。如果可以进行,那么checkcast指令不会改变操作数栈,否则它会抛出ClassCastException异常。

2.3 生成桥接方法支持多态

​ 首先简单看一下什么是桥接方法(Bridge Method)又叫作合成方法(Synthetic Method),其实际是编译器合成的方法,其内部会调用我们在类中定义的方法。先来看一下例子,然后对桥接方法有个感性的认识。

interface Animal {
   Animal getAnimal();
}
class Dog implements Animal {
  @Override
  public Dog getAnimal() {
    return new Dog();
  }
}
复制代码

上面定义了一个Animal接口,其定义了一个返回值为Animal的getAnimal方法,然后实现Dog实现了Animal接口,但在方法实现上getAnimal方法的返回值类型缩小为Dog类型。仔细地朋友会注意到getAnimal方法上加了Override注释,但根据重写的定义方法名、方法返回值、方法参数个数与类型应该都一样时才算方法的重写。理论上面的代码在IDEA上应该出现红色警告报错,但实际上并不会。 这是由于JDK1.5之后,重写的定义被扩大了,方法的返回值类型可以改变,但必须是原方法返回值的子类。这个过程中编译器会生成桥接方法,其伪代码如下。

class Dog implements Animal {
  public Dog getAnimal() {
    return new Dog();
  }
  //Bridge method generated by the compiler
  public Animal getAnimal() {
    return ((Dog)this).getAnimal();
  }
}
复制代码

通过javap -c Dog.class 查看字节码可以看到字节码中有两个getAnimal方法。

public Dog getAnimal();
    descriptor: ()LDog;
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: new           #2                  // class Dog
         3: dup
         4: invokespecial #3                  // Method "<init>":()V
         7: areturn
 public Animal getAnimal();
    descriptor: ()LAnimal;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #4                  // Method getAnimal:()LDog;
         4: areturn
复制代码

其中第二个返回类型为Animal的getAnimal方法为编译器生成的桥接方法,在其code属性中**1: invokevirtual #4 **实际调用了返回类型为Dog的getAnimal方法。

注:

**1、descriptor: ()LAnimal; 表示方法描述符,()代表参数为空,LAnimal中L表示返回类型为引用类型,LAnimal中Animal表示返回的引用类型为Animal **

2、flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC 表示方法访问符,ACC_PUBLIC表示该方法为puclic,ACC_BRIDGE表示该方法为桥接方法,ACC_SYNTHETIC表示该方法有编译器生成

​ 为了加深大家对生成桥接方法支持泛型的多态,来看一下如果编译器不生成桥接方法。泛型的多态可能会存在什么问题。首先定义了一个泛型的Node接口,其有一个setData方法。

public class Node<T> {
  private T data;
  public void setData(T data) {
    System.out.println("Node.setData");
    this.data = data;
  }
}
复制代码

然后再定义实现类ConcreteNode,并指定类型参数为Integer,并且ConcreteNode类中重写了setData方法。

public class ConcreteNode extends Node<Integer> {
  @Override
  public void setData(Integer data) { 
    System.out.println("ConcreteNode.setData");
    super.setData(data);
  }
}
复制代码

但根据将前面讲的类型替换的内容,可以知道类型替换完后Node类应该变为下面这样。

public class Node {
  private Object data;
  public void setData(Object data) { this.data = data;}
}
复制代码

​ 这时ConcreteNode的 setData(Integer data) 就没办法Override Node的setData(Object data),因为ConcreteNode方法参数类型为Integer,而Node方法的参数类型为Object。这就使得ConcreteNode中的setData(Integer data) 变成了方法重载了。那到底ConcreteNode中的 setData(Integer data)是方法重写还是方法重载? 仔细地朋友又会注意到setData(Integer data)方法上加了Override注释,并且上面的代码在IDEA中并不会出现红色警告报错。很显然编译器认为这个是方法重写而不方法重载,如果是方法重载加上Override注释IDEA是会出现红色警告报错的。

​ 还是老办法通过 javap -v ConcreteNode.class 查看字节码,看看编译器到底做了些什么。

 public void setData(java.lang.Integer);
    descriptor: (Ljava/lang/Integer;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: invokespecial #2                  // Method Node.setData:(Ljava/lang/Object;)V
         5: return
      LineNumberTable:
        line 11: 0
        line 12: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   LConcreteNode;
            0       6     1  data   Ljava/lang/Integer;

  public void setData(java.lang.Object);
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: checkcast     #3                  // class java/lang/Integer
         5: invokevirtual #4                  // Method setData:(Ljava/lang/Integer;)V
         8: return
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   LConcreteNode;
复制代码

可以看到经过编译后,最终字节码中有二个setData方法,第一个为ConcreteNode代码中的setData方法,第二个为java编译器生成的桥接方法,且并在第二个方法内部调用了第一个setData方法,即第二个方法对应的下面这行指令。

5: invokevirtual #4                  // Method setData:(Ljava/lang/Integer;)V
复制代码

实际上java编译器生成的桥接方法伪代码如下。

public class ConcreteNode extends Node {
  public void setData(Integer data) { 
    System.out.println("ConcreteNode.setData");
    super.setData(data);
  }
  //Bridge method generated by the compiler
  public void setData(Object data) {
    setData((Integer) data);
  }
  }
}
复制代码

3 泛型中通配符该如何使用

	学习泛型最让人头疼的是上界限通配符与下界限通配符。在简要地介绍上界限通配符与下界限通配符前。有必要先了解一下编程语言中的不变(invariance)、协变(covariance)与逆变(contravariance)。
复制代码
3.1 不变、协变与逆变

​ 从本质上讲编程语言中的不变、协变、逆变描述了子类型关系如何受到类型转换的影响,其实际围绕着一个核心的问题子类型是否可以隐性地转换为父类型,即子类型变量是否能隐性地赋值给父类型变量。如果有类型A与B,以及类型转换f,同时≤ 表示子类型关系(如 A ≤ B表示A是B的子类型),那么

  • 如果从 A ≤ B 能推导出 f(A) ≤ f(B)f 是协变的。
  • 如果从 A ≤ B 能推导出 f(B) ≤ f(A) f 是逆变的。
  • 如果上面的都不成立则f是不变的。

​ 看完上面的定义还有有点懵,来看看一个具体的例子。假设 f(A) = List<A> 且List定义如下

class List<T> { ... } 
复制代码

那么f是不变、协变还是逆变呢?不变意味着List 是List的子类;逆变意味着 List 是 List 的子类型;不变意味着List 与 List 之间不存在子类型关系。很显然java中泛型是不变的,因为 List 与 List 之间不存在子类型关系。

​ 再来看看另外一个例子,假设 f(A) = A[] , 那么f是不变、协变还是逆变呢?不变意味着Integer[] 是Number[]的子类;逆变意味着 Number[]是Integer[] 的子类型;不变意味着Integer[] 与 Number[] 之间不存在子类型关系。很显然java中数组是协变的,因为 Integer[]是Number[]的子类型。可能你还是有点懵,来抓住核心子类型变量是否能隐性地赋值给父类型变量看一下就明白了。

List<Number> numberList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
//编译报错,java泛型是不变,子类型变量不能隐性地赋值给父类型变量
numberList = integerList;
//编译报错,java泛型是不变,父类型变量不能隐性地赋值给子类型变量
integerList = numberList;
Number[] numberArray = new Number[]{};
Integer[] integerArray = new Integer[]{};
//编译正常,java数组是协变的,子类型变量不能隐性地赋值给父类型变量
numberArray = integerArray;
复制代码

​ 那为什么java中不支持List类型变量赋值给List类型变量呢?一个放Nubmer的容器,不能放Integer?

List<Number> numberList = new ArrayList<>();
//编译正常
numberList.add(new Integer(1));
List<Integer> integerList = new ArrayList<>();
//编译报错,java泛型是不变,子类型变量不能隐性地赋值给父类型变量
numberList = integerList;
复制代码

List可以放入Integer,但却不能将List类型变量赋值给List类型变量。试想如果java泛型支持协变会发生什么?

List<Number> numberList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
//假设java泛型是协变,编译通过
numberList = integerList;
//原本就是编译正常的
numberList.add(new BigDecimal("9.9"));
复制代码

如果java泛型是协变,那么就相当于在一个integerList的容器里面能放入BigDecimal的变量,那么这就与前面提到的java引入泛型是为了加强类型检测就矛盾了,BigDecimal都能认为是Integer的类型,子类都不是呀。

​ 前面提到java引入泛型的另一个目的是支持泛型编程,泛型编程的最大优势在于代码的复用性更强。现在如果想定义一个给某一类型数组排序的方法。

import java.util.Comparator;
public class Collections {
  public static <T> void sort(List<T> list, Comparator<T> comparator) {...}
}
复制代码

上面Collections类的sort方法签名,似乎已满足我们的需求,通过同时指定sort方法中的待排序的list为泛型与比较器comparator为泛型。这个sort可以处理不同类型数组的排序问题,只要调用时指定对应的类型,然后实现对应类型的比较器。事实真的如此吗?这个方法,在通用性上是不是有待扩展呢?

public class Assets {
  private BigDecimal assetsValue; //资产价值
}
public class Food extends Assets {
  private Integer retentionPeriod; //保质期
}
public class Meat extends Food {
  private BigDecimal fatContent; //脂肪含量
  public Meat() {}
  public Meat(BigDecimal fatContent) { this.fatContent = fatContent;}
}
public class Fruit extends Food {
  private BigDecimal vitaminContent; //维生素含量
  public Fruit() {}
  public Fruit(BigDecimal vitaminContent) { this.vitaminContent = vitaminContent;}
}
//资产价值比较器
public class AssetsValueComparator implements Comparator<Assets> {
  public int compare(Assets o1, Assets o2) {...}
}
//食物保质期比较器
public class FoodRetentionPeriodComparator implements Comparator<Food> {
  public int compare(Food o1, Food o2) {...}
}
//脂肪含量比较器
public class FatContentComparator implements Comparator<Meat> {
  public int compare(Meat o1, Meat o2) {...}
}
//维生素含量比较器
public class VitaminContentComparator implements Comparator<Fruit> {
  public int compare(Fruit o1, Fruit o2) {...}
}
复制代码

上面定义了Assets、Food、Meat、Fruit类,Food的父类是Assets,Meat与Fruit的父类都是Assets。然后分别为Assets、Food、Meat、Fruit根据他们的属性定义了对应的比较器。前面定义的Collections的sort方法,我们可以对存放Food的list根据食物保持期进行排序,这时要指定比较器为FoodRetentionPeriodComparator。

List<Food> foods = new ArrayList<>(); 
foods.add(new Meat()); foods.add(new Fruit());
Collections.sort(foods, new FoodRetentionPeriodComparator());
复制代码

对存放Fruit的list根据水果的维生素含量进行排序,这时要指定比较器为VitaminContentComparator。

List<Fruit> fruit = new ArrayList<>(); 
fruit.add(new Fruit(new BigDecimal("3.00"))); fruit.add(new Fruit(new BigDecimal("7.00")));
Collections.sort(fruit, new VitaminContentComparator());
复制代码

如果现在,我们要对存放食品的list,他们的资产价值进行排序呢?是不是指定比较器为AssetsValueComparator就行呢?

List<Food> foods = new ArrayList<>(); 
foods.add(new Meat()); foods.add(new Fruit());
//编译错误
Collections.sort(food, new AssetsValueComparator());
复制代码

​ 根据排序的方法签名为public static void sort(List list, Comparator comparator),调用排序方法传入的类型参数为Food,所以调用时比较器实际的泛型为Comparator,而 AssetsValueComparator定义时指定的类型参数为Assets,其实际的泛型为Comparator。前面说泛型是不变的,根本不可以将Comparator 类型变量赋值给Comparator变量,那什么情况下才下行呢?除非泛型引入新的东西支持逆变,根据逆变可知道如果Comparator是逆变的,那么Comparator 类型变量能赋值给Comparator变量。为了支持上面场景,java引入了上界通配符<? extends T>与下界配符<? super T> 去分别支持泛型的协变与逆变。有了逆变只要将上面Collections的sort方法签名改为下面那样,前面代码中的编译错误就将消失。

import java.util.Comparator;
public class Collections {
  //comparator参数改为Comparator<? super T>,让其支持逆变
  public static <T> void sort(List<T> list, Comparator<? super T> comparator) {...}
}
List<Food> foods = new ArrayList<>(); 
foods.add(new Meat()); foods.add(new Fruit());
//编译成功
Collections.sort(food, new AssetsValueComparator());
复制代码

如果,打开JDK的java.util.Collections类的源码,查看sort方法的签名,你将发现其方法签名和上面我们自定义的方法签名是一样。

 public static <T> void sort(List<T> list, Comparator<? super T> c) {...}
复制代码
3.2 协变与上界通通配符

​ 有了上面关于不变、协变与逆变的介绍,再来看上界通配符就相对简单多了。java中上界通配符 <? extends T> 用于支持协变,即可以将List 类型的变量赋值给 List<? extends Food>类型的变量, 当然也可以将List类型的变量赋值给 List<? extends Food>类型的变量,只要指定的类型参数是Food的子类就行。上界通配符蕴含了一种最多是类型T的含义。

下界通配符 Plate<?extends Fruit> 覆盖上图中蓝色的区域。

Plate<? extends Fruit> fruitPlate;
Plate<Apple> applePlate = new Plate<>();
//编译正常,上界通配符支持协变
fruitPlate = applePlate;
Plate<Banana> bananaPlate = new Plate<>();
//编译正常,上界通配符支持协变
fruitPlate = bananaPlate;
复制代码

但要注意,由于编译器不知道上界通配符 Plate<? extends Fruit> fruitPlate 类型的变量具体放的是什么类型,其有可以是Fruit,也可以能是Apple,编译器为了安全起见不允许向其中加入元素,否则编译器报错。通常来讲在类与方法体中定义上界通配符 <? extends T> 的变量都是没有意义的,更多在方法入参上定义上界通配符 <? extends T>的参数。

class Plate<T>{
    private T item;
    public Plate(T t){item=t;}
    public void set(T t){item=t;}
    public T get(){return item;}
}
void tackleFruitPlate(Plate<? extends Fruit> fruitPlate) {
  // 编译错误
  fruitPlate.add(new Apple());
  // 编译错误
  fruitPlate.add(new Banana());
  // 编译错误
  fruitPlate.add(new Fruit());
  // 编译成功
  Fruit fruit = fruitBasket.get();
}
复制代码

上面的tackleFruitPlate方法签名使得其即可以接收处理Plate的参数,也可以接收处理Plate的参数,因为上界通配符支持协变。

3.3 逆变与下界通通配符

​ 前面也提到过逆变,以及为java为支持泛型编程的复用性而引入下界通通配符<? super T>去支持逆变。下界通配符蕴含了一种至少是类型T的含义。比如Plate<? super Fruit> plate,其代表这盘子里面放的是水果的概念。由于下界通通配符<? super T>支持逆变所以可以将Plate< Fruit>类型变量与Plate< Food>类型变量赋值给Plate<? super Fruit>类型变量。

Plate<? super Fruit>覆盖下图中红色的区域。

Plate<? super Fruit> plate  = new Plate<>();
Plate<Fruit> fruitPlate = new Plate<>();
//编译正常,下界通配符支持逆变
plate = fruitPlate;
Plate<Food> foodPlate = new Plate<>();
//编译正常,下界通配符支持逆变
plate = foodPlate;
Plate<Apple> applePlate = new Plate<>();
//编译失败,下界通配符不支持协变
plate = applePlate;
复制代码

由于下界通配符蕴含了一种至少是类型T的含义,所以可以给其存入具体的本身对象或子类对象。但要注意其不支持将父类对象存入,同时从中获取元素后,也只能是Object类型。

// plate 是放
Plate<? super Fruit> plate  = new Plate<Fruit>();
//编译正常,可存入本身对象
p.add(new Fruit());
//编译正常,可存入子类对象
p.add(new Apple());
//编译正常失败,不可存入父类对象
p.add(new Food());
//读取出来的东西只能存放在Object类里。
Apple newFruit3 = p.get();    //Error
Fruit newFruit1 = p.get();    //Error
Object newFruit2 = p.get();
复制代码
3.4 再谈PECS原则

​ 有前面这么多介绍再回过头来看看经典的PECS原则,即要生产对象用于获取时会用 <? extends T> ,消费对象用于生成与处理时会用<? super T>。Oracle在其官方文档中将这一原则其实叫做 "in" and "out" principle,这个其实更加贴切易懂。java 为支持协变与逆变,而引入的上界通通配符<? extends T> 与下界通通配符 <? super T> ,里面的extends 与 super本身就相当的晦涩难懂,本来吧协变与逆变是为了方法参数变量赋值的扩展更强才引入的,来了一个extends 与 super着实让接触java泛型的人一脸的懵。相比之下Kotlin中泛型的out 与 in 关键字,则显得更加贴切易懂。如果从变量作用的角度将变量进行归类,变量可归类为in类型变量与out类型变量。

  • in类型变量为代码提供数据。 想象一个带有两个参数的复制方法:copy(src, dest)。 src 参数提供要复制的数据,因此它是“in”参数。
  • “out”类型变量,其一般用于保存其他地方使用的数据。 在复制示例 copy(src, dest) 中,dest 参数接受数据,因此它是“out”参数。

当还有一些变量同时是in类型变量与out类型的变量。在决定是否使用通配符以及哪种类型的通配符合适时,可以使用**"in" and "out" principle**。**"in" and "out" principle ** 遵守下面的规则:

  • 当变量是in类型变量,使用 extends 关键字,使用上限通配符定义,即<? extends T>,也就是PE。
  • 当变量是out类型变量,使用 super 关键字,使用下限通配符定义,即<? super T>,也就是CS。
  • 如果可以使用 Object 类中定义的方法访问in类型变量,则使用无界通配符。
  • 如果要同时支持以in和out类型变量的形式访问变量,则不要使用通配符。

方法的返回参数并不适合采用通配符合,因为这会强制要求开发人员进行类型转换。

java.util.Collections的copy方法签名为我们展示了**"in" and "out" principle**。

public static <T> void copy(List<? super T> dest, List<? extends T> src) {...}
复制代码

dest 参数只为copy方法提供数据,是in类型变量,所以使用了上限通配符定义,即<? extends T>;而src参数只用于保存copy方法的结果,是out类型变量,所以使用下限通配符定义,即<? super T>。前面在介绍分析的list排序方法,java.util.Collections的sort的方法签名同样展示了的**"in" and "out" principle**。

public static <T> void sort(List<T> list, Comparator<? super T> c) {...}
复制代码

参数c用于保存比较过程中的结果,是out类型的变量所以使用下限通配符定义,即<? super T>。

4 反射获取泛型信息

4.1 Class、Method与Fileld上保存的泛型信息

​ 前面通过javap -v 查看字节码的信息时,已知道类、方法、字段的泛型信息实际都保存在字节码的附加属性signature中。打开JDK的Class类、Method类、Constructor类与Fileld类的源码会发现,其里面都保存了对应的泛型信息。

public final class Class<T> implements Serializable, GenericDeclaration, Type, 		  
							AnnotatedElement {
 	  // Generic signature handling
    private native String getGenericSignature0();
    // Generic info repository; lazily initialized
    private volatile transient ClassRepository genericInfo;               
  ...                            
}
public final class Field extends AccessibleObject implements Member { 
    // Generics and annotations support
    private transient String    signature;
    // generic info repository; lazily initialized
    private transient FieldRepository genericInfo;
  ...
}
public final class Method extends Executable { 
		// Generics and annotations support
    private transient String              signature;
    // generic info repository; lazily initialized
    private transient MethodRepository genericInfo;
  	...
}
public final class Constructor<T> extends Executable {
    // Generics and annotations support
    private transient String    signature;
    // generic info repository; lazily initialized
    private transient ConstructorRepository genericInfo;
  	...
}
复制代码

可以看到Class类、Method类、Constructor类与Fileld类内部都有一个对应的Repository 用于保存与获取泛型对应的签名信息。如果再跟踪调用这些Repository方法时,最终会看到一些public的方法,这些方法是JDK提供的用于获取类、方法、构造方法、字段中泛型信息的。

Class类中可以用于获取泛型信息相关的方法有如下,

// 可以用于获取Map<K,V>中的K与V,如果是String这类无泛型信息的,直返回空数组。
public TypeVariable<Class<T>>[] getTypeParameters() {
  ClassRepository info = getGenericInfo();
  if (info != null)
    return (TypeVariable<Class<T>>[])info.getTypeParameters();
  else
    //如果是String这类无泛型信息的类,直返回空数组。
    return (TypeVariable<Class<T>>[])new TypeVariable<?>[0];
}
//可以用于获取父类的泛型信息,如果父类不是泛型,返回对应class类型。
public Type getGenericSuperclass() {
  ClassRepository info = getGenericInfo();
  if (info == null) {return getSuperclass();}
  if (isInterface()) { return null;}
  return info.getSuperclass();
}
//可以用于获取接口的泛型信息,如果接口不是泛型,返回对应class类型。
public Type[] getGenericInterfaces() {
   ClassRepository info = getGenericInfo();
   return (info == null) ?  getInterfaces() : info.getSuperInterfaces();
}
复制代码

Field类中可以用于获取泛型信息相关的方法有,

//可以用于获取成员变量Map<K,V>、T等的泛型信息,如果是String这类非泛型的成员变量,返回对应class类型。
public Type getGenericType() {
  if (getGenericSignature() != null)
    return getGenericInfo().getGenericType();
  else
    //非泛型的成员变量,返回对应class类型
    return getType();
}
复制代码

Method类中中可以用于获取泛型信息相关的方法有,

//可以用于获取泛型方法的类型参数, 如<K, V extends Number> 中的 K与V,如果不是泛型方法,直返回空数组。
public TypeVariable<Method>[] getTypeParameters() {
  if (getGenericSignature() != null)
    return (TypeVariable<Method>[])getGenericInfo().getTypeParameters();
  else
    //非泛型方法,直返回空数组
    return (TypeVariable<Method>[])new TypeVariable[0];
}
//可以用于获取泛型方法的返回值类型参数,如果不是泛型方法,返回对应class类型。
public Type getGenericReturnType() {
  if (getGenericSignature() != null) {
    return getGenericInfo().getReturnType();
  } else { 
    //非泛型方法,返回对应class类型
    return getReturnType();
  }
}
//可以用于获取方法参数的泛型信息,如果方法参数不是泛型,返回对应class类型
public Type[] getGenericParameterTypes() {
  return super.getGenericParameterTypes();
}
复制代码

Constructor类中中可以用于获取泛型信息相关的方法有,

public TypeVariable<Constructor<T>>[] getTypeParameters() {
  if (getSignature() != null) {
    return (TypeVariable<Constructor<T>>[])getGenericInfo().getTypeParameters();
  } else
    return (TypeVariable<Constructor<T>>[])new TypeVariable[0];
}
//可以用于获取构造方法参数的泛型信息,如果构造方法参数不是泛型,返回对应class类型
public Type[] getGenericParameterTypes() {
  return super.getGenericParameterTypes();
}
复制代码

上面涉及Class类、Method类、Constructor类与Fileld类中可以用于获取泛型的方法,已给出简明的注释,如果对应返回并不包括泛型信息,那结果要么是一个空的数组,要么是对应类的class类型,实际Class是Type的子类。

4.2 JDK1.5 引入的 Type 体系

​ 上面在介绍的通过Class类、Method类与Fileld类中的方法获取泛型信息时,经常有看一下返回类型为Type或是TypeVariable。可能你之前还见过ParameterizedType、WildcardType、GenericArrayType这些类型。JDK里面的这些类都是为了支持泛型而引入的。下面其对应的UML类图,可以看到Class、ParameterizedType、WildcardType、GenericArrayType都是实现或继承了Type。

java中Type是比Class更加抽象地概念,JDK源码上是这么表述他的。

Type is the common superinterface for all types in the Java programming language. These include raw types, parameterized types, array types, type variables and primitive types.

TypeVariable 表示类型变量,ParameterizedType表示参数化类型,WildcardType表示通配符类型、GenericArrayType表示范型数组。给和例子,来点更直观的映像。

//类上面的K与V是TypeVariable
public class Node<K, V extends Number> {
  //变量key对应的K类型是TypeVariable
  private K key;
  //变量data对应V的类型是TypeVariable
  private V data;
  //变量subData对应V[]的类型是GenericArrayType
  private V[] subData;
  //变量originalList对应List<V>的类型是ParameterizedType
  private List<V> originalList;
  //泛型方法copy,前面申明的泛型类型参数<T>中的T的类型是TypeVariable
  //copy方法中data参数的List<T>的类型是ParameterizedType
  //copy方法中c参数Comparator<? super T>的类型是ParameterizedType
  public static <T> void copy(List<T> data, Comparator<? super T> c) {...}
}
复制代码

再来看一下TypeVariable、ParameterizedType、WildcardType与GenericArrayType都有什么方法。

public interface TypeVariable<D extends GenericDeclaration> extends Type, AnnotatedElement {
  //获取该泛型变量的上限,如List<? extends Serializable & Comparable<String>>,
  	//将得到Serializable、Comparable<String>的数组
  Type[] getBounds();
   //获取声明该类型变量时的Class、Constructor或Method
  D getGenericDeclaration();
  //获取声明该类型变量的名称, 如Test<A>,将得到A
  String getName();
}
public interface ParameterizedType extends Type {
    //获取<>中的实际类型,如List<String>,将得到String
  	//如Map<String, Interger>,将得到String与Interger 组成的数据
    Type[] getActualTypeArguments();
    //获取擦除后的类型,如List<String>,将得到List
    Type getRawType(); 
    //如果这个类型是某个类型所属,获取这个所属类型,没有返回null; 如Map.Entry<K,V>, 将得到Map
    Type getOwnerType();
}
public interface GenericArrayType extends Type {
   //获得数组元素类型,如List<String>[], 将得到List<String>,如T[],将得到T
  Type getGenericComponentType();
}
public interface WildcardType extends Type {
    //获取范型变量的上界, 如List<? extends Number>,将得到Number
    Type[] getUpperBounds();
    //获取获取范型变量的下界 如List<? super String>,将得到String
    Type[] getLowerBounds();
}
复制代码
4.3 使用Type体系的实用技巧

​ 前面已介绍了Type体系中的TypeVariable、ParameterizedType、WildcardType与GenericArrayType对应的含义与其各自定义的方法。那在使用这个Type体系时有什么技巧不?来看一下我个人总结的部分技巧。

父类方法返回子类型对象

前面介绍桥接方法时,我们引用一个Animal的例子。这里多加一个Animal的实现类Cat。

interface Animal {
   Animal getAnimal();
}
class Dog implements Animal {
  @Override
  public Dog getAnimal() {
    return new Dog();
  }
}
class Cat implements Animal {
  @Override
  public Cat getAnimal() {
    return new Cat();
  }
}
复制代码

​ 我们已知道因为编译器生成桥接方法,上面的子类Dog与Cat在重写getAnimal方法时分别将返回值类型缩小为Dog与Cat 是合法的。但能不能在定义Animal时就将返回值类型缩小为Animal的某一个待定的子类呢?这样子类再重写方法时,也不用每次都手都修改为子类本身。实际上通过泛型可以很方便的实现,我们的需求只要将上面的接口定义重构一下,然后实现类改造一下就行。我把使用泛型的这个技巧归纳为父类方法返回子类型对象

interface Animal<T extends Animal > {
   T getAnimal();
}
class Dog implements Animal<Dog> {
  @Override
  public Dog getAnimal() {
    return new Dog();
  }
}
class Cat implements Animal<Cat> {
  @Override
  public Cat getAnimal() {
    return new Cat();
  }
}
复制代码

​ 看一下Netty框架中核心的类 ServerBootstrap、Bootstrap与AbstractBootstrap上如何使用上面说的这个技巧。ServerBootstrap是Netty中服务端引导类的抽象,Bootstrap则是Netty中客户端引导类的抽象,而AbstractBootstrap则定义了引导类的公共方法与属性。Netty为了使得用户在使用时更加方便,采用了Bulid模式,我们能够方便的采用链式调用设置各种参数最终得到一个服务端或是客户端引导类的实例。

//通过链式调用设置各种参数,便捷的build出ServerBootstrap
ServerBootstrap serverBootstrap = new ServerBootstrap()
   .group(new NioEventLoopGroup(), new NioEventLoopGroup())
   .channel(NioServerSocketChannel.class)
   .option(ChannelOption.SO_BACKLOG, 1024)
   .childOption(ChannelOption.SO_KEEPALIVE, true)
   .childOption(ChannelOption.TCP_NODELAY, true)
   .childHandler(new ChannelInitializer<NioSocketChannel>() {
     protected void initChannel(NioSocketChannel ch) {
       ch.pipeline().addLast(new ServerHandler());
     }
    });
//通过链式调用设置各种参数,便捷的build出Bootstrap
Bootstrap bootstrap = new Bootstrap()
  .group(new NioEventLoopGroup())
  .channel(NioSocketChannel.class)
  .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
  .option(ChannelOption.SO_KEEPALIVE, true)
  .option(ChannelOption.TCP_NODELAY, true)
  .handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) {
      ch.pipeline().addLast(new ClientHandler());
    }
  });
复制代码

从上面的代码中可以看到在构建服务端与客户端引导类的实例时,都调用了group、channel、option等方法,这些方法都是公共的方法,通常的作法是把他抽象到AbstractBootstrap中去,然后为了兼容ServerBootstrap类型与Bootstrap类型,AbstractBootstrap中定义的这些方法的返回值类型应该为AbstractBootstrap。而Netty使用了父类方法返回子类型对象,让其AbstractBootstrap定义的方法直接返回对应的子类,如果子类是ServerBootstrap就返回ServerBootstrap,如果子类是Bootstrap就返回Bootstrap。

public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable {
   public B group(EventLoopGroup group) {
    ObjectUtil.checkNotNull(group, "group");
    if (this.group != null) {
      throw new IllegalStateException("group set already");
    } else {
      this.group = group;
      return this.self();
    }
  }
  //返回自己
  private B self() {
    return (B) this;
  }
  ...
}
复制代码

可以看到AbstractBootstrap将泛型类型参数定义为B extends AbstractBootstrap<B, C> 与 C extends Channel,然后定义了一下返回自已的self方法。

//传入Bootstrap给类型参数B,AbstractBootstrap中的self将返回Bootstrap
public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
	...
}
//传入ServerBootstrap给类型参数B,AbstractBootstrap中的self将返回ServerBootstrap
public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel> {
	...
}
复制代码

获取传递给泛型父类的实际类型

​ 假设现在定义了一个泛型的AbastactRepository抽象类,以及对应的UserRepository与ClientRepository实现类,如下。

public abstract class AbastactRepository<T> {
  private Class<T> entityClass;
  public abstract T findByKey(String key);
}
public class UserRepository extends AbastactRepository<UserEntity> {
  public UserEntity findByKey(String key);
}
public class ClientRepository extends AbastactRepository<ClientEntity> {
  public ClientEntity findByKey(String key);
}
复制代码

如何获取UserRepository与ClientRepository,传递给泛型父类的实际类型UserEntity与ClientEntity对应的类,与就是 AbastactRepository中成员变量entityClass的值怎么获取。还记得前面介绍Class类获取泛型的方法getGenericSuperclass不,实现我们可以通过这个利用ClientRepository.class 调用 getGenericSuperclass方法获取AbastactRepository这个ParameterizedType,然后再利用ParameterizedType的getActualTypeArguments方法获取到ClientEntity便得到我们想要的类型信息啦。

ParameterizedType type = (ParameterizedType) ClientRepository.class.getGenericSuperclass(); 
//clazz实例便是ClientEntity对应class的实例
Class clazz = (Class) type.getActualTypeArguments()[0];
复制代码

那能不能再通用一点呢?只要在AbastactRepository中拿到对应子类的class实例,便可以像上面一样调用etGenericSuperclass获取到ParameterizedType。AbastactRepository中怎么拿到对应子类的class实例?其实可以通过Object.getClass()方法,该方法获取到的是运行时该AbastactRepository的class实例,也就是说如果运行时AbastactRepository是UserRepository对象,返回的就是UserRepository对应class实例,如果AbastactRepository是ClientRepository对象,返回的就是ClientRepository对应class实例,所以上面的代码可以进一步抽像为。

public abstract class AbastactRepository<T> {
  private Class<T> entityClass;
  public abstract T findByKey(String key);
  public AbastactRepository() {
  	this.entityClass = (Class<T>)((ParameterizedType) getClass()
  		.getGenericSuperclass()).getActualTypeArguments()[0];
  }
}
复制代码

我把上面这个技巧称为获取传递给泛型父类的实际类型

缩小重写方法入参类型

现在定义了一个上下文接口Context与一个标签构建接口LabelBuilder,Context接口定义一个getFutureTasks其返回上下文的一些处理结果,而LabelBuilder接口定义了一个构建标签的接口参数类型是Context。现在我们想LabelBuilder的不同具体实现,接收到的参数类型缩小为对应的Context。

public interface Context {
	List<Future> getFutureTasks();
}
public class ScenarioAContext implements Context {
  public List<Future> getFutureTasks() { return null; }
}
public class ScenarioBContext implements Context {
  public List<Future> getFutureTasks() { return null;}
}
public interface LabelBuilder {
  void build(Context context);
}
复制代码

比如上面的ScenarioAContext时,其对应的ScenarioALableBuilder实现只接收ScenarioAContext类型的参数而不是Context这种太抽象的参数,于是我们有了下面的这个类定义。

public class ScenarioABuilder implements LabelBuilder {
  //编译失败
  @Override
  public void build(ScenarioAContext context) {...}
}
复制代码

很遗憾编译没通过,因为上面方法的定义已不符合重写的规范了,那去掉将上面的@Override注解去掉呢?很遗憾编译会提示你还有一个方法没有实现。那有什么方法处理这种场景呢。其实很简单,不知道你是否还记得前面说的编译器的类型擦除包括什么中的说的编译器生成桥接方法的部分。如果记得你会发现,那个正常能实现我们这个场景。所以我们修改一个LabelBuilder接口的定义与ScenarioABuilder的定义。

public interface LabelBuilder<T extends Context> {
  void build(T context);
}
public class ScenarioABuilder implements LabelBuilder<ScenarioAContext> {
  //编译成功
  @Override
  public void build(ScenarioAContext context) {...}
}
复制代码

好了一切完美。我把上面这个技巧归纳为缩小重写方法入参类型。最后给大家留一下思考题,下面LabelBuilder接口定义有什么区别?

public interface LabelBuilder<T extends Context> {
  void build(T context);
}
public interface LabelBuilder<T> {
  void build(T context);
}
复制代码

总结

​ 本文详细地介绍了java泛型相关知识,包括为什么引入泛型、编译器为泛型做了什么处理、泛型本身为什么不支持协变、如何让泛型支持协变,怎么通过反射获取泛型信息本文并没介绍泛型的使用的一些限制,感兴趣的可以参考Oracle的官方文档 Restrictions on Generics。下一篇中我将介绍一下Spring中的ResolvableType对泛型的抽象,ResolvableType使得操作泛型变量更加简洁。

结尾

原创不易,点赞、在看、转发是对我莫大的鼓励,关注公众号洞悉源码是对我最大的支持。同时相信我会分享更多干货,我同你一起成长,我同你一起进步。

参考

**The Java™ Tutorials - Generics **

Java Generics - Bridge method?

Covariance, Invariance and Contravariance explained in plain English?

Why I distrust wildcards and why we need them anyway

**字节码增强技术探索 **

深入理解JVM(四)——虚拟机执行子系统

猜你喜欢

转载自juejin.im/post/7067697898753884196