概述

1. 泛型定义

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

2. 举个栗子

List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);

for(int i = 0; i< arrayList.size();i++){
    String item = (String)arrayList.get(i);
    Log.d("泛型测试","item = " + item);
}

如果你幸运的运行的上面的代码,不出意外。绝对是会报错的就像下面这样

java.lang.ClassCastException:java.lang.Integercannot be cast to java.lang.String

上面的代码首先实例化一个ArrayList对象,它可以存放所有Object及其子类实例。分别add一个Integer类型对象和String类型对象,我们原本以为list中存放的全部是Integer类型对象,于是在使用get()方法获取对象后进行强制转换。从代码中可以看到,索引值为1的位置放置的Integer类型,很显然在进行强制转换时会抛出ClassCastException(类型转换异常)。由于这种异常只会发生在运行时,我们在开发时稍有不慎,就会直接掉到坑里,还很难排查出问题。

  1. 集合本身无法对其存放的对象类型进行限定,可以涵盖Java中的所有类型。各路牛鬼蛇神都可以装进去。
  2. 由于我们要使用的实际存放类型的方法,所以不可避免地要进行类型转换。小对象转大对象很容易,大对象转小对象则有很大的风险,因为在编译时,我们无从得知对象真正的类型。

泛型即为了解决这类问题所存在的。

泛型的定义和使用

一个泛型类(generic class)就是具有一个或多个类型变量的类。上面的例子中的List就是一个典型的泛型类。泛型类的定义结构类似下面的代码:

public class ClassName<T1, T2> {
    // 可以任意多个类型变量

public void doSomething(T1 t1) {
    System.out.println(t1);
    }
}

注意,在Java编码规范中,类型变量通常使用较短的大写字母,并且最好与其作用相匹配。譬如:List中的变量使用E,对应单词Element,Map中的K,V变量对应单词Key和Value。当然这些都是约定性质的东西,其实类型变量的命名规则与Java中的普通变量命名规则是一致的。

下面的代码使用上面定义的泛型类,就是这么简单。

ClassName<String, String> a = new ClassName<String, String>();
a.doSomething("hello world");

泛型接口的定义和使用
接口本质上来说就是一种特殊的类,所以泛型接口的定义和使用与泛型类相差无几。下面的代码是泛型接口的定义和使用。

public interface InterfaceName<T1, T2> { // 可以任意多个类型变量

public void doSomething(T1 t1);
}

public class ConcreteName<T2> implements InterfaceName<String, T2> {

public void doSomething(String t1) {
    System.out.println(t1);
    }
}


InterfaceName<String, String> a = new ConcreteName<String>();
a.doSomething("hello world");

从上面的例子可以看出,如果实现一个泛型接口,可以在定义时直接传入具体的类型(如T1传入String),也可以继续传入一个类型,待使用时再确认具体的类型。

泛型方法的定义和使用
泛型类和泛型接口的类型变量都是定义在类型级别,其作用域可覆盖成员变量和成员方法。泛型方法的类型参数定义在方法签名中,一个典型的泛型方法定义如下:

//
// 创建一个指定类型的无参构造的对象实例。
//@param <T> 待创建对象的类型。
//@param t 指定类型所对应的Class对象。
//@return 返回创建的对象。
//@throws Exception
//
public <T> T getObject(Class<T> t) throws Exception {
    return t.newInstance();
}

上面的代码中表示这是一个泛型方法,T是仅作用于getObject方法上的类型变量。在调用这个方法时,传入具体的类型。

String newStr = generic.getObject(String.class);

泛型变量的类型限定
假定我们有个需求,需要编写一个获取两个对象中较大的对象的泛型方法,利用上面的泛型知识,编写出下面的代码。

public <T> T getMax(T t1, T t2) {
    if (t1.compareTo(t2) > 1) { // 编译错误
        return t1;
        } else {
            return t2;
        }
    }

在上面的代码无法通过编译,由于我们都没有对类型变量对任何的约束限制,那么实际上这个类型可以是任意Object及其子类。那么在使用这个类型变量时,只能调用Object类中的方法。而Object本身就是Java中对顶层的类,没有实现Comparable接口,所以无法调用compareTo方法来比较对象的大小。这时候可以通过限定类型变量来达到目的。

public <T extends Comparable<T>> T getMax(T t1, T t2) {
    if (t1.compareTo(t2) > 1) {
        return t1;
        } else {
            return t2;
    }
}

注意到上面的代码使用extends关键字限定了类型变量T必须继承自Comparable,于是变量t1和t2就可以使用Comparable接口中的compareTo方法了。

不管是泛型类、泛型接口还是泛型方法,都可以进行类型限定。类型限定的特点如下:

不管该限定是类还是接口,统一都使用extends关键字。
使用&符号进行多个限定,那么传入的具体类型必须同时是这些类型的子类。

public <T extends Serializable&Cloneable&Comparable> T getMax(T t1, T t2) {
    ...
}

由于Java中不支持多继承,所以不存在一个同时继承两个以上的类的类。所以,在泛型的限定中,&连接的类型最多只能有一个类,而接口数量则没有限制。同时,如果同时限定类和接口,则必须将类写在最前面。

public <T extends Object&Serializable&Cloneable&Comparable> T getMax(T t1, T t2) { // 合法
    ...
}

public <T extends Object&ArrayList> T getMax(T t1, T t2) { // 同时限定两个类,不合法
    ...
}

public <T extends Serializable&Cloneable&Comparable&Object> T getMax(T t1, T t2) { // 将类写在最后面 ,不合法
    ...
}

泛型的实现原理

前面介绍了泛型的基础知识以及使用方法,下面将更加深入地介绍泛型的底层原理。

Java中的泛型是伪泛型
泛型思想最早在C++语言的模板(Templates)中产生,Java后来也借用了这种思想。虽然思想一致,但是他们存在着本质性的不同。C++中的模板是真正意义上的泛型,在编译时就将不同模板类型参数编译成对应不同的目标代码,ClassName和ClassName是两种不同的类型,这种泛型被称为真正泛型。这种泛型实现方式,会导致类型膨胀,因为要为不同具体参数生成不同的类。

Java中ClassName和ClassName虽然在源代码中属于不同的类,但是编译后的字节码中,他们都被替换成原始类型(ClassName),而两者的原始类型的一样的,所以在运行时环境中,ClassName和ClassName就是同一个类。Java中的泛型是一种特殊的语法糖,通过类型擦除实现(后面介绍),这种泛型称为伪泛型。由于Java中有这么一个障眼法,如果没有进行深入研究,就会在产生莫名其妙的问题。值得一提的是,不少大牛对Java的泛型的实现方式很不满意。

类型擦除
Java中的泛型是通过类型擦除来实现的。所谓类型擦除,是指通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且再必要的时候添加类型检查和类型转换的方法。

下面通过两个例子来证明在编译时确实发生了类型擦除。

例1分别创建实际类型为String和Integer的ArrayList对象,通过getClass()方法获取两个实例的类,最后判断这个实例的类是相等的,证明两个实例共享同一个类。

// 声明一个具体类型为String的ArrayList
ArrayList<String> arrayList1 = new ArrayList<String>();  
arrayList1.add("abc");  
// 声明一个具体类型为Integer的ArrayList
ArrayList<Integer> arrayList2 = new ArrayList<Integer>();  
arrayList2.add(123);  

System.out.println(arrayList1.getClass() == arrayList2.getClass());  // 结果为true

例2创建一个只能存储Integer的ArrayList对象,在add一个整型数值后,利用反射调用add(Object o)add一个asd字符串,此时运行代码不会报错,运行结果会打印出1和asd两个值。这时再里利用反射调用add(Integer o)方法,运行会抛出codeNoSuchMethodException异常。这充分证明了在编译后,擦除了Integer这个泛型信息,只保留了原始类型。

ArrayList<Integer> arrayList3 = new ArrayList<Integer>();
arrayList3.add(1);
arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
for (int i = 0; i < arrayList3.size(); i++) {
    System.out.println(arrayList3.get(i)); // 输出1,asd
}

arrayList3.getClass().getMethod("add", Integer.class).invoke(arrayList3, 2); // NoSuchMethodException:java.util.ArrayList.add(java.lang.Integer)

自动类型转换

上一节上说到了类型擦除,Java编译器会擦除掉泛型信息。那么调用ArrayList的get()最终返回的必然会是一个Object对象,但是我们在源代码并没有写过Object转成Integer的代码,为什么就能“直接”将取出来的对象赋予一个Integer类型的变量呢(如下面的代码第12行)?

import java.util.List;
import java.util.ArrayList;

//
// 泛型中的类型转换测试。
//
public class Test {

    public static void main(String[] args) {
        List<Integer> a = new ArrayList<Integer>();
        a.add(1);
        Integer ai = a.get(0);
    }
}

实际上,Java的泛型除了类型擦除之外,还会自动生成checkcast指令进行强制类型转换。上面的代码中的main方法编译后所对应的字节码如下。

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: iconst_1
        10: invokestatic  # 4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        13: invokeinterface # 5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
        18: pop
        19: aload_1
        20: iconst_0
        21: invokeinterface # 6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
        26: checkcast     # 7                  // class java/lang/Integer
        29: astore_2
        30: return
        LineNumberTable:
        line 7: 0
        line 8: 8
        line 9: 19
        line 10: 30
    }

看到第18行代码就是将Object类型的对象强制转换为Integer的指令。我们完全可以将上面的代码转换为下面的代码,它所实现的效果跟上面的泛型是一模一样的。既然泛型也需要进行强制转换,所以泛型并不会提供运行时效率,不过可以大大降低编程时的出错概率。

public static void main(String[] args) {
    List a = new ArrayList();
    a.add(1);
    Integer ai = (Integer)a.get(0);
}