Java 泛型、协变、逆变

泛型

从一个问题开始,在 Java 中能否将一个 ArrayList<Integer> 变量赋值给一个 ArrayList<Object> 变量。我们可以做下实验:

首先,声明一个 ArrayList<Integer> 类型的变量:

List<Integer> li = new ArrayList<>();

接着将 li 赋值给一个 List<Object> 变量:

List<Object> lo = li;

如果我们编译上述代码,会得到如下编译错误:

Error:(12, 25) java: 不兼容的类型: java.util.List<java.lang.Integer>无法转换为java.util.List<java.lang.Object>

数组

那能否将一个 Integer 类型的数组赋值给一个 Object 类型的数组呢?我们可以继续实验:

首先声明一个 Integer 类型的数组:

Integer[] s = new Integer[10];

然后将 s 赋值给一个 Object 类型的数组,并修改数组的第一个值:

Object[] o = s;
o[0] = 1;
System.out.println(o[0]);

我们会发现上述代码可以编译运行,最终输出:

1

如果我们向 o 放入其它类型的元素:

o[1] = "Hello, World";

上述可以通过编译,但是运行时则会抛出 ArrayStoreException

协变、逆变和不变

要理解上述现象,首先我们必须了解协变、逆变和不变的概念。

如果有两个类型参数 S 和 T,其中 S 是 T 的子类;考虑数组类型,那么协变、逆变和不变可以理解为:

  • 协变(Covariant):S[] 是 T[] 的子类
  • 逆变(Contravariant):T[] 是 S[] 的子类
  • 不变(Invariant):S[] 不是 T[] 的子类,T [] 也不是 S[] 的子类

我们知道 Java 的泛型是基于类型擦除。JVM 底层不知道任何关于泛型的信息,由编译器完成以下工作:

  • Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.
  • Insert type casts if necessary to preserve type safety.
  • Generate bridge methods to preserve polymorphism in extended generic types.

使用类型擦除,Java 代码不需要像 C++ 那样为每一个参数化生成一个类。(因为 JVM 不知道泛型的任何信息,所以 JVM 上其它静态语言,如 Scala 等也是基于类型擦除)

根据 Generics gotchas 中的描述,Java 泛型是不变(Invariant)而不是协变(Covariant)的。也就是说,虽然可以认为 String 是 Object ,但是不能认为 ArrayList<String> 是 ArrayList<Object>。所以不能将一个 ArrayList<String> 变量赋值给 ArrayList<Object>,这将导致编译错误。

而 Java 的数组确是协变的,这就是为什么我们可以将 Integer[] 赋值给 Object[] 而不会产生编译错误。只不过在运行时,JVM 会记录每个数组的实际类型,并且每次更新数组都会做类型检查。如果我们错误放入了其他类型的值,那么只有在运行时才能发现。

泛型的局限

Java 数组被设计成协变,一个原因是可以实现多态。一个例子是:

static long sum(Number[] numbers) {
    long summation = 0;
    for (Number number : numbers) {
        summation += number.longValue();
    }
    return summation;
}

我们可以向 sum 传入任何 Number 子类型的数组:

Integer[] myInts = {1, 2, 3, 4, 5};
Long[] myLongs = {1L, 2L, 3L, 4L, 5L};
Double[] myDoubles = {1.0, 2.0, 3.0, 4.0, 5.0};
System.out.println(sum(myInts));
System.out.println(sum(myLongs));
System.out.println(sum(myDoubles));

但是如果我们将数组换成泛型集合:

static long sum(List<Number> numbers) {
    long summation = 0;
    for (Number number : numbers) {
        summation += number.longValue();
    }
    return summation;
}

那么就不能像数组一样随意调用:

List<Integer> myInts = asList(1, 2, 3, 4, 5);
List<Long> myLongs = asList(1L, 2L, 3L, 4L, 5L);
List<Double> myDoubles = asList(1.0, 2.0, 3.0, 4.0, 5.0);
System.out.println(sum(myInts)); //compiler error
System.out.println(sum(myLongs)); //compiler error
System.out.println(sum(myDoubles)); //compiler error

因为泛型是不变的(Invariant),限制了多态的实现。不过,我们还有 wildcard type。借助 wildcard type,我们可以实现多态:

static long sum(List<? extends  Number> numbers) {
    long summation = 0;
    for (Number number : numbers) {
        summation += number.longValue();
    }
    return summation;
}

现在 List<Integer>,List<Long> 和 List<Double> 都可以传给 sum 函数了。

使用 wildcard 实现协变

使用 wildcard 实现协变,我们可以将类型参数声明为 ? extends T,其中 T 是已确定的子类。

一般来说,使用 wildcard 实现协变,我们只能从集合中读取元素,而不能向集合中写入元素。如可以声明以下集合:

List<? extends Number> myNums = new ArrayList<Integer>();

从 myNums 读取元素:

Number n = myNums.get(0);

? extends Number 表示这是一个未知类型,但是该类型一定是 Number 的子类,所以我们可以从 myNums 读取元素并赋值给父类 Number 变量。

但是,我们不能向 myNums 加入元素:

myNums.add(45L); //compiler error

因为编译器只知道 myNums 中元素都是 Number 的子类,但是不能确定这些元素是 Integer、Long、FLoat 还是其他合法 Number 子类。

使用 wildcard 实现逆变

使用 wildcard 实现逆变,我们可以将类型参数声明为 ? super T,其中 T 是已确定的子类。

一般来说,使用 wildcard 实现逆变,我们只能向集合中加入元素,而不能从集合中读取元素。如可以声明以下集合:

List<? super Number> myNums = new ArrayList<>();
myNums.add(1L);  // legal
myNums.add(0.1);  // legal

但是,我们不能从 myNums 读取元素:

Number myNum = myNums.get(0); //compiler-error

Get/Put 原则

总的来说,如果我们只想从集合中读取元素,那么应该使用协变;如果我们只想向集合中加入元素,那么应该使用逆变。

当然,我们可以将两者结合。一个例子是实现一个函数,将一个列表的 Number 复制到另一个列表中:

public static void copy(List<? extends Number> source, List<? super Number> destiny) {
    for(Number number : source) {
        destiny.add(number);
    }
}

在函数中,我们只从 source 读取元素,只向 destiny 加入元素。然后,我们可以这样使用 copy 函数:

List<Integer> myInts = asList(1,2,3,4);
List<Integer> myDoubles = asList(3.14, 6.28);
List<Object> myObjs = newArrayList<Object>();
copy(myInts, myObjs);
copy(myDoubles, myObjs);

参考

  1. Introduction to generic types in JDK 5.0
  2. Covariance and contravariance
  3. Generics gotchas
  4. Covariance and Contravariance In Java

Previous post: 理解 Java Executor 框架

Next post: 理解 select、poll 和 epoll