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

你真的了解java中的泛型吗?iptor Ljava lang Integer V flags ACC PUBLIC Code stack 2 locals 2 args size 2 0 aload 0

欢迎大家来到IT世界,在知识的湖畔探索吧!

iptor: (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 且List定义如下

class List
   
   
   
   
   
   
     { ... } 复制代码 
   

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

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

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

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

List
   
   
   
   
   
   
     numberList = new ArrayList<>(); List 
    
      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 
   
   
   
   
   
   
     void sort(List 
    
      list, Comparator 
     
       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
   
   
   
   
   
   
     { public int compare(Assets o1, Assets o2) {...} } //食物保质期比较器 public class FoodRetentionPeriodComparator implements Comparator 
    
      { public int compare(Food o1, Food o2) {...} } //脂肪含量比较器 public class FatContentComparator implements Comparator 
     
       { public int compare(Meat o1, Meat o2) {...} } //维生素含量比较器 public class VitaminContentComparator implements Comparator 
      
        { 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
   
   
   
   
   
   
     foods = new ArrayList<>(); foods.add(new Meat()); foods.add(new Fruit()); Collections.sort(foods, new FoodRetentionPeriodComparator()); 复制代码 
   

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

List
   
   
   
   
   
   
     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
   
   
   
   
   
   
     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引入了上界通配符
与下界配符
去分别支持泛型的协变与逆变。有了逆变只要将上面Collections的sort方法签名改为下面那样,前面代码中的编译错误就将消失。

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

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

 public static 
   
   
   
   
   
   
     void sort(List 
    
      list, Comparator 
      c) {...} 复制代码 
     
   

3.2 协变与上界通通配符

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

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

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

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

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

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

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

3.3 逆变与下界通通配符

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

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

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

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

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

// plate 是放
Plate
   
   
   
   
   
    plate  = new Plate
   
   
   
   
   
   
     (); //编译正常,可存入本身对象 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原则,即要生产对象用于获取时会用
,消费对象用于生成与处理时会用
。Oracle在其官方文档中将这一原则其实叫做 “in” and “out” principle,这个其实更加贴切易懂。java 为支持协变与逆变,而引入的上界通通配符
与下界通通配符
,里面的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 关键字,使用上限通配符定义,即
,也就是PE。 当变量是out类型变量,使用 super 关键字,使用下限通配符定义,即
,也就是CS。 如果可以使用 Object 类中定义的方法访问in类型变量,则使用无界通配符。 如果要同时支持以in和out类型变量的形式访问变量,则不要使用通配符。

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

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

public static 
   
   
   
   
   
   
     void copy(List 
     dest, List 
     src) {...} 复制代码 
   

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

public static 
   
   
   
   
   
   
     void sort(List 
    
      list, Comparator 
      c) {...} 复制代码 
     
   

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

4 反射获取泛型信息

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

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

public final class Class
   
   
   
   
   
   
     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 
    
      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,如果是String这类无泛型信息的,直返回空数组。 public TypeVariable 
    
      >[] getTypeParameters() { ClassRepository info = getGenericInfo(); if (info != null) return (TypeVariable 
     
       >[])info.getTypeParameters(); else //如果是String这类无泛型信息的类,直返回空数组。 return (TypeVariable 
      
        >[])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
   
   
   
   
   
   
     、T等的泛型信息,如果是String这类非泛型的成员变量,返回对应class类型。 public Type getGenericType() { if (getGenericSignature() != null) return getGenericInfo().getGenericType(); else //非泛型的成员变量,返回对应class类型 return getType(); } 复制代码 
   

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

//可以用于获取泛型方法的类型参数, 如
   
   
   
   
   
   
     中的 K与V,如果不是泛型方法,直返回空数组。 public TypeVariable 
    
      [] getTypeParameters() { if (getGenericSignature() != null) return (TypeVariable 
     
       [])getGenericInfo().getTypeParameters(); else //非泛型方法,直返回空数组 return (TypeVariable 
      
        [])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
   
   
   
   
   
   
     >[] getTypeParameters() { if (getSignature() != null) { return (TypeVariable 
    
      >[])getGenericInfo().getTypeParameters(); } else return (TypeVariable 
     
       >[])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中的泛型吗?

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
   
   
   
   
   
   
     { //变量key对应的K类型是TypeVariable private K key; //变量data对应V的类型是TypeVariable private V data; //变量subData对应V[]的类型是GenericArrayType private V[] subData; //变量originalList对应List 
    
      的类型是ParameterizedType private List 
     
       originalList; //泛型方法copy,前面申明的泛型类型参数 
      
        中的T的类型是TypeVariable //copy方法中data参数的List 
       
         的类型是ParameterizedType //copy方法中c参数Comparator 
        的类型是ParameterizedType public static 
        
          void copy(List 
         
           data, Comparator 
           c) {...} } 复制代码 
          
         
        
       
      
     
   

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

public interface TypeVariable 
   
     extends Type, AnnotatedElement {   //获取该泛型变量的上限,如List 
    >, //将得到Serializable、Comparable 
    
      的数组   Type[] getBounds(); //获取声明该类型变量时的Class、Constructor或Method   D getGenericDeclaration();   //获取声明该类型变量的名称, 如Test 
     
   ,将得到A   String getName(); } public interface ParameterizedType extends Type { //获取<>中的实际类型,如List 
   
     ,将得到String //如Map 
    
      ,将得到String与Interger 组成的数据 Type[] getActualTypeArguments(); //获取擦除后的类型,如List 
     
       ,将得到List Type getRawType(); //如果这个类型是某个类型所属,获取这个所属类型,没有返回null; 如Map.Entry 
      
        , 将得到Map Type getOwnerType(); } public interface GenericArrayType extends Type { //获得数组元素类型,如List 
       
         [], 将得到List 
        
          ,如T[],将得到T   Type getGenericComponentType(); } public interface WildcardType extends Type { //获取范型变量的上界, 如List 
         ,将得到Number Type[] getUpperBounds(); //获取获取范型变量的下界 如List 
         ,将得到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 getAnimal(); } class Dog implements Animal 
    
      { @Override public Dog getAnimal() { return new Dog(); } } class Cat implements Animal 
     
       { @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
   
   
   
   
   
   
     () { 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 
    
      () { @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, 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

与 C extends Channel,然后定义了一下返回自已的self方法。

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

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

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

public abstract class AbastactRepository
    
    
    
    
    
    
      { private Class 
     
       entityClass; public abstract T findByKey(String key); } public class UserRepository extends AbastactRepository 
      
        { public UserEntity findByKey(String key); } public class ClientRepository extends AbastactRepository 
       
         { 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
    
    
    
    
    
    
      { private Class 
     
       entityClass; public abstract T findByKey(String key); public AbastactRepository() { this.entityClass = (Class 
      
        )((ParameterizedType) getClass() .getGenericSuperclass()).getActualTypeArguments()[0]; } } 复制代码 
       
      
    

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

缩小重写方法入参类型

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

public interface Context {
	List
    
    
    
    
    
    
      getFutureTasks(); } public class ScenarioAContext implements Context { public List 
     
       getFutureTasks() { return null; } } public class ScenarioBContext implements Context { public List 
      
        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
    
    
    
    
    
    
      { void build(T context); } public class ScenarioABuilder implements LabelBuilder 
     
       { //编译成功 @Override public void build(ScenarioAContext context) {...} } 复制代码 
      
    

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

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

总结

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

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/89479.html

(0)
上一篇 14小时前
下一篇 14小时前

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们YX

mu99908888

在线咨询: 微信交谈

邮件:itzsgw@126.com

工作时间:时刻准备着!

关注微信