微信号:QunarTL

介绍:Qunar技术沙龙是去哪儿网工程师小伙伴以及业界小伙伴们的学习交流平台.我们会分享Qunar和业界最前沿的热门技术趋势和话题;为中高端技术同学提供一个自由的技术交流和学习分享平台.

java 泛型解析

2018-09-26 08:00 罗招材

罗招材


个人介绍:罗招材,2016 年加入去哪儿网技术团队。目前在火车票事业部/技术部,参与开发了出票系统服务拆分和架构升级,火车票冷热数据自动分离数据库中间件。个人对数据库、系统性能优化、高并发的系统很感兴趣。

一、引言

java 从 JDK1.5 开始引入泛型,泛型的本质是参数化类型,也就是在定义时用一个类型形参来表示类型,并在使用时传入实际的类型。引入泛型主要有下面三个方面的好处。

  1. 增强编译时期的类型检查,让类型转换问题在编译时暴露而非等到运行时异常;

  2. 减少强制类型转换的代码,让 java 编译器为我们做隐式的类型转换工作;

  3. 能编写更加通用的代码,让代码算法和具体的数据类型解耦, 例如 java 的集合框架。

下面通过一个例子直观地感受一下,在原来没有泛型时通常怎么用 List 的。

【未使用泛型】

 
           
  1. @Test

  2. public void nonGenericListTest() {

  3.    List stringList = new ArrayList();        // 期望保存String类型的列表

  4.    stringList.add("hello");

  5.    stringList.add(1);                        // 编译通过

  6.    String s1 = (String) stringList.get(0);   // 类型转换

  7.    String s2 = (String) stringList.get(1);   // 运行时ClassCastException异常

  8. }

使用泛型之后的使用方式如下,可以看出在运行时才能暴露的异常,在编译期就能发现,同时也少了类型转换的代码。

【使用泛型】

 
           
  1. @Test

  2. public void genericListTest() {

  3.    List<String> list = new ArrayList<String>();e

  4.    list.add("hello");

  5.    list.add(1);                        // 编译期错误

  6.    String s = list.get(0);             // 无需类型转换

  7. }

二、使用方式

java 泛型可用在接口、类以及方法中,其定义方式我们都很熟悉,例如定义一个泛型接口。

【泛型接口】

 
           
  1. public interface GenericInterface<T> {

  2.    void doSomething(T t);

  3. }

定义泛型类和泛型方法。

【泛型类&泛型方法】

 
           
  1. /** 泛型类 */

  2. public class GenericClass<T> {

  3.    private T obj;

  4.    public void put(T t) {

  5.        this.obj = t;

  6.    }

  7.    public T get() {

  8.        return this.obj;

  9.    }

  10.    /** 泛型方法 */

  11.    static <V> void swap(V l, V r) {

  12.        V tmp = l;

  13.        l = r;

  14.        r = tmp;

  15.    }

  16. }

三、实现原理

3.1 泛型的实现方式

通常实现泛型的方式通常有两种思路:

  1. 模板展开,编译时进行类型膨胀, 也就是为每一种参数类型按照泛型类模板生成一个类;

  2. 共享代码,编译时将类型参数擦除,所有的参数类型都共用一份代码,并在使用时在需要的地方加上强制类型转换。

模板展开的方式能保留泛型的参数信息,但是会导致类型膨胀,共享代码的方式代码复用性好,但是会在运行时丢失泛型的类型信息。

3.2 java 泛型实现

java 泛型的实现方式选择了基于擦除的方式实现,其主要原因是基于兼容性的考虑。java 中强调的是二进制向后兼容性(Binary Backwards compatibility,链接 https://docs.oracle.com/javase/specs/jls/se8/html/jls-13.html#jls-13.2),也就是在低版本例如 java1.2 中可以运行的 class 文件在高版本例如 java 6/7/8 的 JRE(包括 JVM 和标准库)上也要能正常运行。java 采用基于擦除的方式处理具有更好的兼容性,以前非泛型的类库仍然可以使用,并可以非常小的代价接入泛型的方式。更详细的背后的考量原因推荐参考 java 架构师 Brian Goetz 所写的这篇文章 State of the Specialization (http://cr.openjdk.java.net/~briangoetz/valhalla/specialization.html),以及他在JVMLS 2015上的演讲文档

(http://www.oracle.com/technetwork/java/jvmls2015-goetz-2637900.pdf) 。

3.2.1 泛型的有效期

java 泛型只是会在 java 编译器进行识别和处理, java 编译器处理完生成的 class 文件中的运行时字节码已经没有了泛型的信息,也就是说 JVM 在运行时对于泛型类的处理和普通类没有任何区别。这里强调泛型类的作用期是在 java 编译期间,是因为这对我们理解泛型类的原理和后面讲到的泛型类的种种限制很有帮助。

3.2.2 编译过程

首先简单了解一下 java 编译器的处理过程,详情请参考 《深入理解 java 虚拟机》 以及 R 大的分享中关于编译器的部分内容。(http://www.valleytalk.org/wp-content/uploads/2011/05/Java_Program_in_Action_20110727.pdf)

 1.解析与填充符号表

  1. 词法分析,将源文件字符流转换为 token 序列

  2. 语法分析,构建抽象语法树,后续的分析如注解处理等也都是基于抽象语法数进行

  3. 填充符号表,将编译期间多个过程的都需要用到的信息用类似 key-value 的形式保存起来

2.注解处理器,执行注解处理器的逻辑,修改抽象语法树(可以改变类型定义,创建新类等, 例如: lombok,链接 https://projectlombok.org/)

3.分析和代码生成

  1. 属性和标注检查,检查类型使用前是否申明、类型匹配检查和常量折叠 

  2. 数据流分析,检查所有语句可达、检查所有受检异常都被捕获或者被抛出等 

  3. 将泛型擦除为原生类型(row type),同时插入必要的转换代码 

  4. 解除语法糖, 将一些高级的语言 feature 例如 foreach、断言和自动装箱/拆箱等特性转换为简单语法结构 

  5. 代码生成,生成 class 文件(包括 class 文件结构信息、元数据信息和字节码)

上述流程图是 java 编译器的将源文件编译成 class 文件的主要流程。其源码可以在 openjdk 中查看,路径为

langtools/src/classes/com/sun/tools/javac/main/JavaCompiler.java,处理逻辑如下。

注:图片来自《深入理解 java 虚拟机》

java 的泛型是基于类型擦除实现的,这个过程是在上述 java 编译器中的分析阶段完成的,也就是说泛型的只是在编译期有效,在编译生成的 class 文件中的字节码已经不包含泛型的信息了(class 文件的元数据信息中还保留了泛型的信息,这个后面再说),编译器对泛型的处理主要包括三部分内容: 

  1. 类型擦除,将泛型类型转换为原生类型(raw type) 

  2. 插入类型转换指令 

  3. 生成桥方法(Bridge Method)

下面分别说明这三个部分的内容。

3.2.3 类型擦除

在说明之前我们需要先说明一下泛型类型中涉及到的几个名词,原生类型、类型形参、类型实参代表的含义,如在下的代码实例中:

  • 类型形参,指的是在定义泛型类时,用于指代类型的形式参数,在例子中就是指 中的 <T> 参数

  • 原生类型,指的是在定义泛型类时,不包含类型参数的类,在例子中就是 ArrayList 类

  • 类型实参,指的是在使用泛型类时,需要用一个实际的类型参数替换定义时的类型形参,在例子就是指 <String>  中的 String 类

【参数说明】

 
           
  1. // 泛型的定义

  2. public class ArrayList<T>{};

  3. // 泛型的使用

  4. ArrayList arrayList = new ArrayList<String>();

类型擦除原理比较简单,主要有两项内容, 1.将泛型类型转换为其原生类型, 例如将 ArrayList<T>  转换为原生类型 ArrayList

2.将泛型类中的类型参数 T 用其指定的界限类型替换 

  1. 对于 <T extends XXX>  指定了上界类型的形参类型来说,编译器将泛型类中的所有 T 都擦除为 XXX 类型, 例如  <T extends List>  则将 T 全部替换为 List 

  2. 对于 <T>  未指定界限类型,则默认用 Object 实参类型替换

例如对于下面这个泛型类。

【 泛型Node类】

 
           
  1. public class Node<T> {

  2.    private T data;

  3.    private Node<T> next;

  4.    public Node(T data, Node<T> next) {

  5.        this.data = data;

  6.        this.next = next;

  7.    }

  8.    public T getData() { return data; }

  9.    public Node<T> getNext() { return next; }

  10. }

我们用 javap 命令查看其生成的 class 文件。

【反编译命令】

 
           
  1. javap -c -s -verbose Test.class

得到的 class 文件如下。

【擦除之后的 class 文件】

 
           
  1. public class com.train.tc.generic.Node<T extends java.lang.Object> extends java.lang.Object

  2.  minor version: 0

  3.  major version: 51

  4.  flags: ACC_PUBLIC, ACC_SUPER

  5. Constant pool:

  6.   #1 = Methodref          #5.#30         // java/lang/Object."<init>":()V

  7.   #2 = Fieldref           #4.#31         // com/train/tc/generic/Node.data:Ljava/lang/Object;

  8.   #3 = Fieldref           #4.#32         // com/train/tc/generic/Node.next:Lcom/train/tc/generic/Node;

  9.   #4 = Class              #33            // com/train/tc/generic/Node

  10.   #5 = Class              #34            // java/lang/Object

  11.   // 省略部分常量池内容

  12.   #30 = NameAndType        #13:#35        // "<init>":()V

  13.   #31 = NameAndType        #6:#7          // data:Ljava/lang/Object;

  14.   #32 = NameAndType        #10:#11        // next:Lcom/train/tc/generic/Node;

  15.   #33 = Utf8               com/train/tc/generic/Node

  16.   #34 = Utf8               java/lang/Object

  17.   #35 = Utf8               ()V

  18. {

  19.  public com.train.tc.generic.Node(T, com.train.tc.generic.Node<T>);

  20.    descriptor: (Ljava/lang/Object;Lcom/train/tc/generic/Node;)V

  21.    flags: ACC_PUBLIC

  22.    Code:

  23.      stack=2, locals=3, args_size=3

  24.         0: aload_0

  25.         1: invokespecial #1                  // Method java/lang/Object."<init>":()V

  26.         4: aload_0

  27.         5: aload_1

  28.         6: putfield      #2                  // Field data:Ljava/lang/Object;

  29.         9: aload_0

  30.        10: aload_2

  31.        11: putfield      #3                  // Field next:Lcom/train/tc/generic/Node;

  32.        14: return

  33.      LineNumberTable:

  34.      // 省略行号内容

  35.      LocalVariableTable:

  36.        Start  Length  Slot  Name   Signature

  37.            0      15     0  this   Lcom/train/tc/generic/Node;

  38.            0      15     1  data   Ljava/lang/Object;

  39.            0      15     2  next   Lcom/train/tc/generic/Node;

  40.      LocalVariableTypeTable:

  41.        Start  Length  Slot  Name   Signature

  42.            0      15     0  this   Lcom/train/tc/generic/Node<TT;>;

  43.            0      15     1  data   TT;

  44.            0      15     2  next   Lcom/train/tc/generic/Node<TT;>;

  45.    Signature: #20                          // (TT;Lcom/train/tc/generic/Node<TT;>;)V

  46.  public T getData();

  47.    descriptor: ()Ljava/lang/Object;

  48.    flags: ACC_PUBLIC

  49.    Code:

  50.      stack=1, locals=1, args_size=1

  51.         0: aload_0

  52.         1: getfield      #2                  // Field data:Ljava/lang/Object;

  53.         4: areturn

  54.      LineNumberTable:

  55.        line 16: 0

  56.      LocalVariableTable:

  57.        Start  Length  Slot  Name   Signature

  58.            0       5     0  this   Lcom/train/tc/generic/Node;

  59.      LocalVariableTypeTable:

  60.        Start  Length  Slot  Name   Signature

  61.            0       5     0  this   Lcom/train/tc/generic/Node<TT;>;

  62.    Signature: #23                          // ()TT;

  63.  public com.train.tc.generic.Node<T> getNext();

  64.    descriptor: ()Lcom/train/tc/generic/Node;

  65.    flags: ACC_PUBLIC

  66.    Code:

  67.      stack=1, locals=1, args_size=1

  68.         0: aload_0

  69.         1: getfield      #3                  // Field next:Lcom/train/tc/generic/Node;

  70.         4: areturn

  71.      LineNumberTable:

  72.        line 18: 0

  73.      LocalVariableTable:

  74.        Start  Length  Slot  Name   Signature

  75.            0       5     0  this   Lcom/train/tc/generic/Node;

  76.      LocalVariableTypeTable:

  77.        Start  Length  Slot  Name   Signature

  78.            0       5     0  this   Lcom/train/tc/generic/Node<TT;>;

  79.    Signature: #26                          // ()Lcom/train/tc/generic/Node<TT;>;

  80. }

  81. Signature: #27                          // <T:Ljava/lang/Object;>Ljava/lang/Object;

从 class 文件中我们可以看出,Node 中T形式类型参数已经被替换为了 Object 实际类型,例如 line:13 的常量池中 #31 的 data 的类型,以及 line:47 的 getData() 方法的返回值类型都是 Object。在生成的 class 文件中,类的内部变量、方法参数以及方法内的局部变量都被擦除替换了,也就是在运行时这些泛型信息就已经消失。但是细心的同学会发现,class 文件中其实还包含了泛型参数 T 的信息,也就是在 LocalVariableTypeTable(可选的用于调试时使用的信息

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.14 )和 Signature

(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.9)中保存的,引用 JVM 规范中对 Signature 解释如下,意思是 JVM 类加载时不会校验这些信息,虽然这些信息可以在运行时通过反射接口获取,但是对运行时不会产生影响,而只是做为类的描述信息存在,至于为何要在保留这部分信息可以参考这里(https://www.zhihu.com/question/36645143)。

Oracle's Java Virtual Machine implementation does not check the well-formedness of Signature attributes during class loading or linking. Instead, Signature attributes are checked by methods of the Java SE platform class libraries which expose generic signatures of classes, interfaces, constructors, methods, and fields. Examples include getGenericSuperclass in Class and toGenericString in java.lang.reflect.Executable.

泛型类擦除之后的类与如下代码等价,其实如果你查看下面代码生成的 class 文件会发现,他们的字节码是一致的。

【泛型擦除后 Node 等价类】

 
           
  1. public class Node {

  2.    private Object data;

  3.    private Node next;

  4.    public Node(Object data, Node next) {

  5.        this.data = data;

  6.        this.next = next;

  7.    }

  8.    public Object getData() { return data; }

  9.    public Node getNext() { return next; }

  10. }

3.2.4 隐式类型转换

如上所述,由于在擦除处理后的类中类型都已经替换为了界定类型(未使用 extends 界定时通常是 Object 类型),在使用时如果需要转换为界定类型的子类,这时编译器会插入一个 checkcast 指令进行隐式的类型转换,使得我们就不再需要写强制转换的代码了。

例如,我们使用前面 Node 泛型类,定义一个实际类型为 Integer 的 Node。

【泛型类使用】

 
           
  1. public class Test {

  2.    public static void main(String[] args) {

  3.        Node<Integer> integerNode = new Node<>(1, null);

  4.        int data = integerNode.getData();  // 这里发生了隐式的类型转换

  5.    }

  6. }

我们通过反编译得到其 class 文件如下,从第 16 行、18 行和 20 行中分别可以看到其使用的过程。 

  1. 调用泛型类的 getData 方法,注意返回值是 Object 类型,也就是擦除之后的类型;

  2. 插入类型转换指令将擦除后端 Object 类型转换为我们的定义的 Integer 类型;

  3. 自动拆箱,因为我们的返回值是 int。

【字节码】

 
           
  1. public class com.train.tc.generic.Test {

  2.  public com.train.tc.generic.Test();

  3.    // 省略构造函数

  4.  public static void main(java.lang.String[]);

  5.    descriptor: ([Ljava/lang/String;)V

  6.    Code:

  7.       0: new           #2                  // class com/train/tc/generic/Node

  8.       3: dup

  9.       4: iconst_1

  10.       5: invokestatic  #3                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

  11.       8: aconst_null

  12.       9: invokespecial #4                  // Method com/train/tc/generic/Node."<init>":(Ljava/lang/Object;Lcom/train/tc/generic/Node;)V

  13.      12: astore_1

  14.      13: aload_1

  15.      // 调用泛型类getData方法,返回值为Object

  16.      14: invokevirtual #5                  // Method com/train/tc/generic/Node.getData:()Ljava/lang/Object;

  17.      // 插入类型转换指令转换为我们指定的Integer类型

  18.      17: checkcast     #6                  // class java/lang/Integer

  19.      // 自动拆箱,因为我们的返回值是int

  20.      20: invokevirtual #7                  // Method java/lang/Integer.intValue:()I

  21.      23: istore_2

  22.      24: return

  23. }

3.2.5 Bridge Methods

在编译器处理泛型时在需要时会自动创建一种叫桥方法(Bridge Method)的方法,那为什么要引入桥方法呢?我们看下面这种情况。

定义一个泛型接口。

【泛型接口】

 
           
  1. public interface Super<T> {

  2.    void doSomething(T t);

  3. }

定义一个实现类如下。

【实现类】

 
           
  1. public class Sub implements Super<String> {

  2.    @Override

  3.    public void doSomething(String s) {

  4.        System.out.println("do something in sub class");

  5.    }

  6. }

那按照上述类型擦除的机制,接口将被等价擦除成如下类。

【擦除后的接口】

 
           
  1. public interface Super {

  2.    void doSomething(Object t);

  3. }

通过字节码也可以验证接口的方法类型已经被擦除并替换为了 Object。

【泛型接口的class文件】

 
           
  1.  major version: 51

  2.  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT

  3. Constant pool:

  4.   // 省略常量池内容

  5. {

  6.  public abstract void doSomething(T);

  7.    descriptor: (Ljava/lang/Object;)V       // 方法的参数被擦除替换为了Object

  8.    flags: ACC_PUBLIC, ACC_ABSTRACT

  9.    Signature: #6                           // (TT;)V

  10. }

  11. Signature: #7                           // <T:Ljava/lang/Object;>Ljava/lang/Object;

  12. SourceFile: "Super.java"

那现在问题就来了,我们的本意是要 Sub 类实现接口 Sup 类的方法doSomething,Sub 类的 doSomething 方法的参数为 String,但其实现的接口 Sub 类的参数却是 Object,两者的方法签名不同,不符合 java 对于方法实现的规范。那编译器是如何处理的呢?

答案可以在 Sub 实现类的编译后的 class 文件中找到答案,在 Sub 的 class 文件中可以看到有两个 doSomething 方法,第一 doSomething 方法是 Sub 类定义的方法参数是 String 类型, 而第二个 doSomething 方法就是编译器自动生成的方法,其参数和接口的参数一直都是 Object,这个自动生成的方法做的事情就是将参数隐式类型转换为 Sub 的类型,并调用 Sub 类的 doSomething 方法。 

【Sub 实现类的 class 文件】

 
           
  1. public class com.train.tc.generic.Sub extends java.lang.Object implements com.train.tc.generic.Super<java.lang.String>

  2.  minor version: 0

  3.  major version: 51

  4.  flags: ACC_PUBLIC, ACC_SUPER

  5. Constant pool:

  6.    // 省略常量池

  7. {

  8.  public com.train.tc.generic.Sub();

  9.    descriptor: ()V

  10.    flags: ACC_PUBLIC

  11.    Code:

  12.      stack=1, locals=1, args_size=1

  13.         0: aload_0

  14.         1: invokespecial #1                  // Method java/lang/Object."<init>":()V

  15.         4: return

  16.      LineNumberTable:

  17.        line 6: 0

  18.      LocalVariableTable:

  19.        Start  Length  Slot  Name   Signature

  20.            0       5     0  this   Lcom/train/tc/generic/Sub;

  21.  // Sub类中的实现方法

  22.  public void doSomething(java.lang.String);

  23.    descriptor: (Ljava/lang/String;)V

  24.    flags: ACC_PUBLIC

  25.    Code:

  26.      stack=2, locals=2, args_size=2

  27.         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;

  28.         3: ldc           #3                  // String do something in sub class

  29.         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

  30.         8: return

  31.      LineNumberTable:

  32.        line 9: 0

  33.        line 10: 8

  34.      LocalVariableTable:

  35.        Start  Length  Slot  Name   Signature

  36.            0       9     0  this   Lcom/train/tc/generic/Sub;

  37.            0       9     1     s   Ljava/lang/String;

  38.  // 编译器生成的桥方法,调用Sub类的实现方法

  39.  public void doSomething(java.lang.Object);

  40.    descriptor: (Ljava/lang/Object;)V

  41.    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC   //  其中ACC_BRIDGE就是桥方法的标识

  42.    Code:

  43.      stack=2, locals=2, args_size=2

  44.         0: aload_0

  45.         1: aload_1

  46.         2: checkcast     #5                  // class java/lang/String

  47.         5: invokevirtual #6                  // Method doSomething:(Ljava/lang/String;)V

  48.         8: return

  49.      LineNumberTable:

  50.        line 6: 0

  51.      LocalVariableTable:

  52.        Start  Length  Slot  Name   Signature

  53.            0       9     0  this   Lcom/train/tc/generic/Sub;

  54. }

  55. Signature: #23                          // Ljava/lang/Object;Lcom/train/tc/generic/Super<Ljava/lang/String;>;

通过编译器处理后的 Sub 等价于如下代码,编译器自动生成并实现了一个和接口 Super 中声明的方法签名一致的桥方法,在桥方法中直接调用 Sub 类中的实现方法。

【Sub 类编译后等价代码】

 
           
  1. public class Sub implements Super<String> {

  2.    @Override

  3.    public void doSomething(String s) {

  4.        System.out.println("do something in sub class");

  5.    }

  6.    // 等价编译器生成的桥方法

  7.    public void doSomething(Object t) {

  8.        this.doSomething((String) t);

  9.    }

  10. }

所以引入桥方法的原因就是,当一个类实现了一个泛型接口类或者继承了一个泛型父类,并且由于编译器的类型擦除机制将方法的参数类型变的不一致时,为了符合 java 方法复写的规范而做的补偿。通常我们并不需要关心这种转换,因为编译器已经自动做了所有的工作,但是了解原理可以让我们在看到相关的调用栈时对多出来的一次方法调用不会感到疑惑。

四、泛型的类型系统

在 java 中,类型的体系结构是基于我们熟悉的继承和实现建立的,根据Liskov替换原则

(https://en.wikipedia.org/wiki/Liskov_substitution_principle),子类可以替换父类,例如 Integer 类型是继承自 Number 类,当需要 Number 时传入一个 Integer 类型的是不会有任何问题的,但反过来当需要 Integer 时传入一个 Number 则需要强制类型转换,此时编译器不再保证运行时不出异常。这种子类可以替换父类的方式,对于 java 数组类型也成立,即 Long[] 可以替换 Integer[]。

但是泛型的类型继承系统通常不那么直观,有时甚至是反直觉的,例如 Number 是 Integer 的父类,那在泛型中 ArrayList<Number>  是 ArrayList<Integer>  的父类吗? 答案是并不是,ArrayList<Number>  和 ArrayList<Integer>  之间其实并没有任何继承关系。原因还是泛型的类型擦除,ArrayList<Number>  和 ArrayList<Integer>  经过类型擦除之后都是原始类型(raw type)ArrayList,因此并不存在继承关系。当然也可以通过下面这个例子来理解,假如ArrayList<Number>  是 ArrayList<Integer>  父类,也就是说下述代码第八行成立,那意味着可以往实际上是 ArrayList<Number>  里放入Long 类型的数据,这也违背了泛型的设计原则。 

【泛型的继承关系】

 
           
  1. @Test

  2. public void inheritTest() {

  3.    // 定义一个只运行存放Integer类型的List

  4.    ArrayList<Integer> integerArrayList = new ArrayList<Integer>();

  5.    // 往其中添加一个整数

  6.    integerArrayList.add(1);

  7.    // Attention: 假如他们的继承关系成立 (实际上这行会编译错误)

  8.    ArrayList<Number> numberList = integerArrayList;

  9.    // 添加一个Long类型数据

  10.    numberList.add(100L);

  11. }

4.1 通配符

但在很多时候我们需要这种泛型的继承关系,例如希望下面这个泛型方法可以打印所有的 Number 类型的列表,但类似 ArrayList<Integer>  类型的列表又不能直接传入这个方法,难道要为每种类型的都写一个方法?显然不是采用这种解决方案,而是引入了泛型通配符的概念,用符号 <?> 表示 一种未知的实参类型 ,这个未知实参类型可以是一组类型实参类型中的任何一个,逻辑上 ArrayList<?> 是 ArrayList<所有实参类型> 的父类, 包括 ArrayList<Integer> 、ArrayList<Long>  和 ArrayList<String>  等。需要注意的是 ArrayList<?> 并不与 ArrayList<Object> 等价 ,事实上 ArrayList<?> 也是 ArrayList<Object> 的父类。

【泛型方法】

 
           
  1. @Test

  2. public void printList(ArrayList<Number> list) {

  3.    for (Number number : list) {

  4.        System.out.println(number);

  5.    }

  6. }

通配符按照包含范围划分可以分为三种:

  1. 无界通配符,也就是 <?> 这种表示形式,含义是表示全部实参集合中的任意一种实参类型;

  2. 上界通配符,用 externs 关键字确定上界, 类似 <? externs Number> 可以表示包含 Number 以及其所有子类的类型组成的集合中的任何一种实参类型(注意这里的 extends 关键字并不是我们常见的继承的含义,他只是复用 extends 关键字来界定范围);

  3. 下界通配符,用 super 关键字确定下界,类似 <? super Number> 可以表示包含 Numbe r以及其所有父类的类型组成的集合中的任何一种实参类型。

但是使用上下限界配符通也会有限制,例如用上界 ArrayList<? externs Number> list = new ArrayList<Long> () 定义的 list,我们调用 list.get() 往外取数据是可以的,因为编译器知道取出来的数据一定是 Numbe r的子类,但是当我们要往其中添加数据时,例如 list.add(1L),编译器是不允许这种操作的,原因是 ArrayList<? externs Number> 对于编译器来说,它并不能确定其真实代表的实参是哪一种子类,这样不能保证类型安全。例如如下的例子,list 代替的真实的实参是 Integer 类型的,如果运行往 list 中添加数据的话,则有可能往实际是 Integer 类型的列表里添加 Double 类型的数据,因此编译器禁止这样的操作。

【通配符】

 
           
  1. ArrayList<Integer> arrayList = new ArrayList<>();

  2. arrayList.add(1);

  3. ArrayList<? extends Number> list = arrayList;

  4. list.add(1.0);  // 编译错误

同样的原理,我们可以知道使用 <? super Number> 下界通配符时则只能往里写,而不能往外读。这个特性在《Effective Java》中称为 PECS (Producer Extends Consumer Super) 原则(https://stackoverflow.com/questions/2723397/what-is-pecs-producer-extends-consumer-super),含义是需要频繁往外读用上界 extends 通配符, 需要频繁往里放的使用下界 super 通配符。其实这种通配符运用场景非常广泛,特别是在集合处理时,如下将一个集合中的数据拷贝到另一个集合的方法,就很好的诠释了他们的用法。

【java.util.Collections】

 
           
  1. // 集合拷贝

  2. public static <T> void copy(List<? super T> dest, List<? extends T> src) {

  3.    int srcSize = src.size();

  4.    if (srcSize > dest.size())

  5.        throw new IndexOutOfBoundsException("Source does not fit in dest");

  6.    if (srcSize < COPY_THRESHOLD ||

  7.        (src instanceof RandomAccess && dest instanceof RandomAccess)) {

  8.        for (int i=0; i<srcSize; i++)

  9.            dest.set(i, src.get(i));

  10.    } else {

  11.        ListIterator<? super T> di=dest.listIterator();

  12.        ListIterator<? extends T> si=src.listIterator();

  13.        for (int i=0; i<srcSize; i++) {

  14.            di.next();

  15.            di.set(si.next());

  16.        }

  17.    }

  18. }

4.2 泛型继承体系

根据前面的叙述可以知道,引入 java 泛型后对原来的继承体系有了一定的影响。引入泛型之后的类型系统其实是有了两个维度:一个是泛型类原生类型的继承体系结构,另一个是类型实参参数自身的继承体系结构。例如对于 List<Long>  和 Collection<? extends Number> 来说, 泛型类自身的继承是指 Collection 是 List 的父类,实参类型自身的继承是指 List<? extends Number> 是 List<Long>  的父类。也就是可以理解为泛型将原来一维的继承关系,扩展成了两维的继承体系。他们之间遵循以下规则

  • 原生类型(raw type),也就是泛型擦除后的类,是其所有泛型类的父类

  • 相同实参参数的泛型类的关系取决于泛型类原生类型的继承体系结构。即 List  是 Collection  的子类,List  可以替换 Collection 。这种情况也适用于带有上下界的类型声明,例如 List<String> 是 Collection<String> 的子类型

  • 当泛型的实参类型不一致时,其子类型可以在两个维度上分别展开。例如对 Collection<? extends Number> 来说,其子类型可以在 Collection 这个维度上展开,即 List<? extends Number> 和 Set<? extends Number> 等;也可以在 Number 这个层次上展开,即 Collection<Long>  和 Collection<Integer>  等,如此循环下去

  • 如果泛型类中包含多个类型参数,则对于每个类型参数分别应用上面的规则

下图清晰的展示了泛型类的继承体系,更加详尽的内容推荐查看 GenericsFAQ 中关于 Type System 部分的内容。

(http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ201)

五、java 泛型的限制

在实际使用泛型时,我们通常会碰到很多限制,但当理解了其实现原理之后会发现这些奇奇怪怪的限制根源都来自于下面两点:

1. 泛型的擦除机制将泛型的参数类型都使用其界定类型替换了,在运行时已经无法确定具体的类型 

2. 泛型在编译时会保证类型安全,当编译器无法确定泛型的使用是类型安全时,java 泛型会禁止这种操作,例如前面的叙述的使用上界类型通配符时不能往里写数据

下面列举几种常见的限制:

1. 泛型的实参类型不能是 java 的原始类型(primitive type),也就是说不能定义 ArrayList<int> 、ArrayList<char>  等。原因对应于泛型擦除将参数类型擦除为 Object,而原始类型与 Object 引用类型之间不能进行转换,因此不合法。

2. 泛型类中不能直接使用 T 来创建对象,类似这种 new T() 创建对象是不合法的。原因还是类型擦除使得运行时无法确定 T 的实际类型,因此无法保证创建出期望的对象,因此是不合法的。

3. 异常捕获中不能使用泛型类,原因还是因为类型擦除,使得泛型类异常 SomeException<Integer>  和 SomeException<String>  擦除变成了同一个类 SomeException,而 catch 无法捕获两个相同的异常类型,因此这种用法也不合法。

【泛型异常】

 
           
  1. try {

  2. // do something

  3. } catch (SomeException<Integer> e) {

  4. } catch (SomeException<String> e) {

  5. }

4.不能使用泛型数组,原因还是泛型编译器无法保证类型安全,具体理解参考官方教程中的例子。

(https://docs.oracle.com/javase/tutorial/java/generics/restrictions.html#createArrays)

5.泛型只是在编译期进行类型安全检查的,它并不能保证运行期是安全的,例如我们可以通过反射直接使用泛型擦除后的类,绕过编译期的检查,此时仍然存在类型安全问题。例如下面的例子,可以通过反射往 Integer 类型的列表中添加 String 类型的数据。原因也正是因为类型擦除导致 ArrayList 中的 add 方法被替换为了 Object 类型的实参,因此可以调用成功。

【反射导致非安全】

 
           
  1. @Test

  2. public void magicTest() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {

  3.    ArrayList<Integer> integers = new ArrayList<>();

  4.    integers.add(1);

  5.    // 反射获取add方法

  6.    Method add = integers.getClass().getDeclaredMethod("add", Object.class);

  7.    // 往其中加入字符串类型的数据

  8.    add.invoke(integers, "string");

  9.    for (Object o : integers) {

  10.        System.out.println(o);

  11.    }

  12. }

  13. /** output

  14. 1

  15. string

  16. */

六、总结

java1.5 之后开始引入了泛型,考虑到兼容性的问题,java 采用了基于类型擦除的方式实现,所有泛型类共享一份代码,泛型只是在编译期有效,增强了编译期的类型检查。同时由于擦除带来的副作用,也就是在运行时丢失了泛化参数信息,也给使用 java 泛型带来了很多限制,总体来说使用泛型还是能让我们编写更安全性和复用更高的代码,例如 java 的集合框架就是最好的范例,此外在各种框架中也广泛的运用着泛型,因此 java 泛型是个很重要的知识点。java 的实现原理还是比较简单的,但是要深刻理解其背后的思想和其带来的各种限制,却需要慢慢体会。

参考文档

1.《thinking in java》关于泛型的部分 2.JavaGenericsFAQ(推荐阅读) : 

http://www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html 3.官方tutorial教程 : 

https://docs.oracle.com/javase/tutorial/java/generics/index.html 4.java 泛型的历史: 

https://www.zhihu.com/question/28665443/answer/118148143 、

https://www.artima.com/scalazine/articles/originsofscala.html, 5.java泛型实现方式的取舍:

http://cr.openjdk.java.net/~briangoetz/valhalla/specialization.html 6.通配符及PECS原则:

https://www.zhihu.com/question/20400700/answer/117464182 、 

https://stackoverflow.com/questions/2723397/what-is-pecs-producer-extends-consumer-super 7.《effective java》关于泛型的部分

8.java编译器原理:

http://www.valleytalk.org/wp-content/uploads/2011/05/JavaPrograminAction20110727.pdf 9.《深入理解java虚拟机》关于编译器原理的部分 10.Java泛型编译器设计&实现:

http://homepages.inf.ed.ac.uk/wadler/gj/Documents/ 11.java的type system : 

http://www.angelikalanger.com/GenericsFAQ/FAQSections/TechnicalDetails.html#FAQ201 12.java获取运行时泛型信息:

http://rednaxelafx.iteye.com/blog/586212 13.java虚拟机规范:

https://docs.oracle.com/javase/specs/jls/se8/html/jls-13.html 14.java泛型的模型&未来实现支持primitive原生类型的方式:

http://www.oracle.com/technetwork/java/jvmls2015-goetz-2637900.pdf 15.Wikipedia中关于泛型的解释: 

https://en.wikipedia.org/wiki/GenericsinJava

 
Qunar技术沙龙 更多文章 从人肉到智能,阿里运维体系经历了哪些变迁? 机器学习之 scikit-learn 开发入门(2) 机器学习之 scikit-learn 开发入门(1) 去哪儿2018-3期技术应届生培训圆满结束 数据价值提升新模式:数据资产管理“AIGOV 五星模型”
猜您喜欢 回家过年,除了团圆饭,还有美猴王 Android属性动画完全解析(上),初识属性动画的基本用法 搞技术的必看!600页阿里技术图册,30位资深大牛解读!(免费下载) 2016年第四季度移动行业数据报告 淘宝技术部世界杯算法大赛赛况