导读
泛型是Java最基础的语法之一,众所周知:出于安全原因,泛型默认不能支持型变(否则会引入危险),因此Java提供了通配符上限和通配符下限来支持型变,其中通配符上限就泛型协变,通配符下限就是泛型逆变。
由于来自早期的设计,所以Java的数组默认就支持型变(Variance):只要A是B的子类,那么A[]就相当于B[]的子类,比如Integer是Number的子类,因此Integer[]就相当于Number[]的子类。
但数组的型变会导致潜在的问题,例如如下程序:
public class ArrayVariance
{
public static void main(String[] args)
{
Integer[] intArr = new Integer[5];
// 数组默认就支持型变,因此下面代码是正确的
Number[] numArr = intArr;
// numArr只要求集合元素是Number,因此下面代码也可通过编译
numArr[0] = 3.4; // ①
}
}
由于Java数组默认支持型变,因此Number[]类型的变量实际引用的数组完全可以是Integer[]、也可是Float[]、也可是Double[]......,因此当程序尝试向 Number[]数组中存入元素时,总有可能导致ArrayStoreException异常——原因就是你永远无法确定Number[]类型的变量实际引用的数组对象。
上面程序编译没有任何问题,但运行上面程序就会导致ArrayStoreException异常。
注意
对于一个强大的编译器来说,如果程序在编译阶段没有警告、没有错误 ,那么运行时就不应该导致简单的语法错误——上面程序编译阶段没有错误,但运行时仅仅只是因为类型不兼容(Java是强类型语言)而出错,这显然是不尽人意的。
泛型默认不支持型变
为了避免重蹈Java数组的覆辙,Java泛型显然不能再继续支持默认的型变。这意味着:即使A是B的子类,那么List<A>也不是List<B>的子类,比如Integer是Number的子类,而List<Integer>却并不是List<Number>的子类。
例如如下程序:
import java.util.*;
public class GenericNoVariance
{
public static void main(String[] args)
{
List<Integer> intList = List.of(2, 4);
// 泛型默认不支持型变,因此下面代码编译错误
List<Number> numList = intList; // ①
numList.add(3.4); // ②
}
}
由于泛型不支持默认的型变,因此上面①号代码在编译阶段就会报错,因此在②号代码就无法在运行时导致错误了。
协变:通配符上限
为了让泛型支持型变,Java引入了通配符上限语法:如果A是B的子类,那么List<A>相当于是List<? extends B>的子类,比如Integer是Number的子类,List<Integer>就相当于List<? extends Number>的子类——这种型变方式被称为协变(covariance)。
对于支持协变的泛型集合,例如List<? extends Number>,Java编译器只知道该List集合的元素是Number的子类——但具体是哪个子类则无法确定。
因此对于协变的泛型集合,程序只能从集合中取出元素——取出的元素的类型肯定能保证是上限;但程序不能向集合添加元素——因此程序无法确定程序要求的集合元素具体是上限的哪个子类。例如如下程序:
import java.util.*;
public class GenericCovariance
{
public static void main(String[] args)
{
List<Integer> intList = new ArrayList<>();
intList.add(2);
intList.add(4);
List<Double> doubleList = new ArrayList<>();
doubleList.add(2.1);
doubleList.add(4.3);
// List<? extends Number>支持协变,
// 因此只要元素是Number子类的List集合,就可以赋值给numList集合
List<? extends Number> numList = intList; // ①
// 取出的元素被当成Number处理
Number n1 = numList.get(0); // ②
System.out.println(n1);
// List<? extends Number>支持协变,
// 因此只要元素是Number子类的List集合,就可以赋值给numList集合
List<? extends Number> numList2 = doubleList; // ①
// 取出的元素被当成Number处理
Number n2 = numList2.get(0); // ②
System.out.println(n2);
}
}
List<? extends Number>是支持协变的泛型,因此程序中两行①号代码可以分别将List<Integer>、List<Double>赋值给List<? extends Number>类型的变量。
List<? extends Number>类型的集合只能取出元素——取出的元素总是被当成Number处理;List<? extends Number>类型的集合不能添加元素——因此编译器无法确定该集合的元素必须是Number的哪个子类。
总结来说,支持协变的集合只能取出元素,不能添加元素——疯狂Java讲义归纳的口诀是:协变只出不进!
对于更通用的泛型来说,对于支持协变的泛型,程序只能调用以泛型为返回值类型的方法;不能调用形参为泛型的方法。例如如下代码。
class Apple<T>
{
private T info;
public Apple(T info) {
this.info = info;
}
public void setInfo(T info) {
this.info = info;
}
public T getInfo() {
return this.info;
}
}
public class GenericCovariance2 {
public static void main(String[] args) {
// 指定泛型T为Integer类型
Apple<Integer> intApp = new Apple<>(2);
// 协变
Apple<? extends Number> numApp = intApp;
// 协变的泛型,调用以泛型为返回值的方法,正确。
// 该方法的返回值是T,该T总是Number类或其子类
Number n = numApp.getInfo();
System.out.println(n);
// 协变的泛型,不能调用以泛型为参数的方法,编译报错
// 因此编译器只能确定T必须是Number的子类,但具体是哪个子类则无法确定,因此编译出错
numApp.setInfo(3); // ①
}
}
上面程序中Apple<? extends Number>也是支持协变的泛型,因此该类型的变量只能调用返回值为泛型的方法,不能调用形参为泛型的方法——如上①号代码所示。
逆变:通配符下限
Java引入了通配符下限语法是为支持逆变(controvariance):如果A是B的父类,那么List<A>反而相当于是List<? super B>的子类,比如Number是Integer的父类,List<Number>反而相当于List<? super Integer>的子类——这种型变方式被称为逆变(contravariance)。
对于支持逆变的泛型集合,例如List<? super Integer>,Java编译器只知道该List集合的元素是Integer的父类——但具体是哪个父类则无法确定。
因此对于逆变的泛型集合,程序只能向集合中添加元素——添加元素的类型总能符合上限——而集合元素总是上限的父类,因此完全没问题;但程序不能从集合中取出元素——因为编译器无法确定集合元素具体是下限的哪个父类——除非你把取出的集合元素总是当成Object处理(众生皆Object)
例如如下程序:
import java.util.*;
public class GenericContravariance
{
public static void main(String[] args)
{
List<Number> numList = new ArrayList<>();
numList.add(2);
numList.add(4.3);
List<Object> objList = new ArrayList<>();
objList.add("Java");
objList.add(3.5f);
// List<? super Integer>支持逆变,
// 因此只要元素是Integer父类的List集合,就可以赋值给intList1集合
List<? super Integer> intList1 = numList; // ①
// 逆变的集合添加元素完全没问题——集合元素肯定是Integer的父类(此处为Number)
intList1.add(20); // ②
System.out.println(intList1);
// List<? super Integer>支持逆变,
// 因此只要元素是Integer父类的List集合,就可以赋值给intList2集合
List<? super Integer> intList2 = objList; // ①
// 逆变的集合添加元素完全没问题——集合元素肯定是Integer的父类(此处为Object)
intList2.add(30); // ②
System.out.println(intList2);
// 取出集合元素时,集合元素只能被当成Object处理
Object ob1 = intList1.get(0);
Object ob2 = intList2.get(0);
}
}
List<? super Integer>是支持逆变的泛型,因此程序中两行①号代码可以分别将List<Number>、List<Object>赋值给List<? super Integer>类型的变量。
List<? super Integer>类型的集合只能添加元素——添加的Integer元素一定匹配下限,而下限一定是集合元素的子类。因此上面两行②号代码都可以添加元素。但如果程序尝试从泛型逆变的集合中取出元素,那么取出的元素只能被当成Object处理(众生皆Object)。
总结来说,支持逆变的集合只能添加元素,不能取出元素(除非取出元素都当成Object)——疯狂Java讲义归纳的口诀是:逆变只进不出!
对于更通用的泛型来说,对于支持逆变的泛型,程序只能调用以泛型为形参的方法;不能调用形参为返回值类型的方法(除非将返回值当成Object处理)。例如如下代码。
class Apple<T>
{
private T info;
public Apple(T info) {
this.info = info;
}
public void setInfo(T info) {
this.info = info;
}
public T getInfo() {
return this.info;
}
}
public class GenericContravariance2 {
public static void main(String[] args) {
// 指定泛型T为Object类型
Apple<Object> objApp = new Apple<>("疯狂Java");
// 逆变
Apple<? super Integer> intApp = objApp;
// 逆变的泛型,调用以泛型为形参的方法,正确。
// 该方法的Integer参数总符合下限,下限一定派生自父类
intApp.setInfo(3);
// 逆变的泛型,调用以泛型为返回值的方法,该返回值只能被当成Object处理
Object o = intApp.getInfo();
System.out.println(o);
}
}
本文结束