泛型定义

在没有泛型前,一旦把一个对象丢进集合中,集合就会忘记对象的类型,把所有的对象都当成 Object 类型处理。当程序从集合中取出对象后,就需要进行强制类型转换,这种转换很容易引起 ClassCastException 异常。

程序在创建集合时指定集合元素的类型。增加了泛型支持后的集合,可以记住集合中元素的类型,并可以在编译时检查集合中元素的类型,如果试图向集合中添加不满足类型要求的对象,编译器就会报错。

泛型类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class Apple<T> {
    private T info;
    public Apple() {}
    public Apple(T info) {
        this.info = info;
    }
    public void setInfo(T info) {
        this.info = info;
    }
    public T getinfo() {
        return this.info;
    }
    public static void main(String[] args) {
        Apple<String> a1 = new Apple<>("Apple");
        System.out.println(a1.getinfo());
        Apple<Double> a2 = new Apple<>(5.67);
        System.out.println(a2.getinfo());
    }
}

泛型方法

定义一个带有类型参数的方法。

类型变量放在修饰符的后面,返回类型的前面。

泛型方法可以定义在普通类中,也可以定义在泛型类中。

1
2
3
4
5
class ArrayAlg {
   public static <T> T getMiddle(T... a) {
     return a[a.length / 2];
   }
}

当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型。

1
ArrayAlg.<String>getMiddle("hello", "world");

大多数情况下,方法调用中可以省略类型参数。编译器可以根据实际的参数类型推导而出。

类型变量的限定

有时类或方法需要对类型变量加以约束。

1
2
3
4
5
6
7
8
9
class ArrayAlg {
   public static <T> T min(T[] a) {
     T smallest = a[0];
     for (int i = 1; i < a.lenght; i++) {
        if (smallest.compareTo(a[i]) > 0) smallest = a[i];
     }
     return a[a.length / 2];
   }
}

变量 smallest 类型为 T,代表它可以是任何类型的对象,但怎么能保证 T 所属的类有 compareTo 方法?解决方法就是将 T 限制为实现了 Comparable 接口的类。

1
public static <T extends Comparable> T min(T[] a)

现在泛型方法只能被实现了 Comparable 接口的类的数组调用。

泛型代码和虚拟机

  • 虚拟机中没有泛型,只有普通类和方法。即类型擦除。
  • 所有的类型参数都用它们的限定类型转换,如果有多个限定类型,以第一个限定类型为准;如果没有给定限定,就用 Object 替换
  • 为保证类型安全性,虚拟机必要时将自动插入强制类型转换
  • 桥方法被合成用来保持泛型擦除后的多态调用(详见 Java 核心技术卷 I)

协变与逆变

逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果 𝐴、𝐵 表示类型,𝑓(⋅) 表示类型转换,≤ 表示继承关系(比如 𝐴≤𝐵 表示 𝐴 是由 𝐵 派生出来的子类)

  • 𝑓(⋅) 是逆变(contravariant)的,当 𝐴≤𝐵 时有 𝑓(𝐵)≤𝑓(𝐴) 成立;
  • 𝑓(⋅) 是协变(covariant)的,当 𝐴≤𝐵 时有 𝑓(𝐴)≤𝑓(𝐵) 成立;
  • 𝑓(⋅) 是不变(invariant)的,当 𝐴≤𝐵 时上述两个式子均不成立,即 𝑓(𝐴) 与 𝑓(𝐵) 相互之间没有继承关系。

数组是协变的

1
2
3
4
Number[] numbers = new Number[3];
numbers[0] = new Integer(10);
numbers[1] = new Double(3.14);
numbers[2] = new Long(99L);

包装类 Integer、Double、Long 是 Number 的子类,numbers 数组中的元素的类型可以是任何 Number 的子类。我们称 Java 数组是协变的 (Covariant)。

不仅如此,下面的代码也是合法的:

1
2
3
Integer[] IntArray = {1,2,3,4};
Number[] NumberArray = IntArray;
Number n = NumberArray[0]; //从一个协变结构中读取元素

根据协变的定义,因为数组是协变的,所以 Integer[] 是 Number[] 的子类。可以将子类型的数组赋予基类型的数组引用。即父类的引用指向子类对象。

但是这会导致一个有趣的问题:

1
2
3
4
Integer[] IntArray = {1,2,3,4};
Number[] NumberArray = IntArray;
NumberArray[0] = 9;
NumberArray[0] = 3.14; //尝试污染一个Integer数组 runtime error

在编译时,上面的代码不会报错,但是运行时最后一行代码会抛出 ArrayStoreException。很明显,即使通过一个 Number[] 引用,也不能将一个浮点数放入一个事实上的 Integer[] 数组。因为在运行时知道这个数组的真实类型为存放 Integer 类型的数组。

泛型是不变的

要理解泛型是不变的,需要在此之前说明泛型的擦除机制。

因为 JDK1.5 中才引入泛型机制,为了兼容旧的字节码,Java 规范在编译时对泛型进行了类型擦除。也就是说我们使用的所有泛型仅仅存在于编译期间,当通过编译器检查后,泛型信息都会被删除。在运行时,JVM 处理的都是没有携带泛型信息的类型。

1
2
3
4
5
6
7
public class ErasedTypeEquivalence {
    public static void main(String[] args) {
        Class c1 = new ArrayList<String>().getClass();
        Class c2 = new ArrayList<Integer>().getClass();
        System.out.println(c1 == c2);
    }
}

上面的执行结果为 true。

尽管 ArrayList<String> 和 ArrayList<Integer> 看上去是不同的类型,但在运行时实际上是相同的类型。这两种类型都被擦除成它们的原生类型,即 ArrayList。

再看下面一段代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Fruit {}
class Apple extends Fruit {}

public class NonCovariantGenerics {
    List<Fruit> flist = new ArrayList<Apple>(); // 编译错误
}

public class CovariantArrays {
    public static void main(String[] args) {
        Fruit[] fruit = new Apple[10]; // 编译正确
}

与数组不同,泛型没有内建的协变类型。虽然 Apple 是 Fruit 的子类,但 ArrayList<Apple> 并不是 List<Fruit> 的子类,NonCovariantGenerics 直接在编译时报错了。

泛型虽然是不变的,但有时需要实现协变和逆变,这时就要用到泛型通配符了。

无限定通配符

在前面说过,泛型是不变的,为了实现泛型的协变与逆变,我们可以使用泛型通配符。

1
2
3
4
5
public void test(List<Object> c) {
    for (int i = 0; i < c.size(); i++) {
        System.out.prinln(c.get(i));
    }
}

这个方法声明没有任何问题,但是调用该方法实际传入的参数值,可能会出错。

考虑如下代码:

1
2
List<String> strList = new ArrayList<>();
test(strList); // 编译出错,因为泛型是不变的, List<String> 并不是 List<Object> 的子类。

为了表示各种泛型 List 的父类,可以使用类型通配符。List<?> 表示元素类型未知的 List。这个 号被称为通配符,它可以匹配任何类型。

将上面的代码,改为如下形式:

1
2
3
4
5
public void test(List<?> c) {
    for (int i = 0; i < c.size(); i++) {
        System.out.prinln(c.get(i));
    }
}

现在传入任何类型的 List,程序可以正常打印集合 c 中的元素。

这种方法同时 又带来了另一个问题,即集合中元素的类型会被当成 Object 类型对待。

泛型通配符的上界

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}

public class GenericsAndCovariance {
    public static void main(String[] args) {
        List<? extends Fruit> flist = new ArrayList<Apple>();
        flist.add(new Apple());  // 编译错误
        flist.add(new Fruit());  // 编译错误
        flist.add(new Object());  // 编译错误
    }
}

现在 flist 的类型是 <? extends Fruit>,extends 指出了泛型的上界为 Fruit。下边界是任意 Fruit 的子类,理论上来说可以是无限多。使用通配符可以将 ArrayList<Apple> 向上转型了,也就实 现了协变。

这样的转换也有一定的副作用。那就是容器的部分功能可能失效。我们不能向一个协变泛型的结构中加入任何元素(除了 null)。观察上面代码,再也不能往容器里任何东西。

因为泛型的类型擦除原因,类型检查移到了编译期,但协变过程又丢掉了具体的类型(可以是 Fruit 类的任何子类,无法确定到底是哪个子类),导致编译器无法确定真实的类型信息,所以拒绝了插入操作。

另一方面,我们知道,不论它是什么类型,它总是 Fruit 的子类型,当我们在读取数据时,能确保得到的数据是一个 Fruit 类型的实例:

1
Fruit get = flist.get(0);

小结:

  • 如果一个容器是只读的,才能协变。不然很容易就能把一些特殊的容器协变到更一般的容器,再往里面添加进不应该储存的类型。
  • 协变结构可读,不可写。

超类型通配符(通配符的下界)

使用通配符 ? super T,其中 T 是一个基类型,或者说父类,我们可以向逆变结构中添加任何 T 及 T 的子类。

逆变结构可写,不可读。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}

public class SuperTypeWildcards {
    static void writeTo(List<? super Apple> apples) {
        apples.add(new Apple());
        apples.add(new Jonathan());
        apples.add(new Fruit());  // 编译错误
    }
}

List<? super Apple> 指定了泛型的下界是 Apple。上边界是模糊的,任意 Apple 的父类都可以,存在无限的可能性,无法确定具体的类型(可以是 Apple 的任何父类,无法确定到底是哪个父类)。所以只能添加 Apple 及其子类。

只能取出 Object 实例:因为我们不知道超类究竟是什么,编译器唯一能保证的只是它是个 Object,因为 Object 是任何 Java 类型的超类。

存取原则

  • 如果你想从一个数据类型里获取数据,使用 <? extends T> 通配符。
  • 如果你想把对象写入一个数据结构里,使用 <? super T> 通配符。
  • 如果你既想存,又想取,那就别用通配符。

通配符图示

pair 类之间没有继承

泛型列表类型中子类型之间的关系

使用通配符的子类型关系