本篇文章将会着重介绍Java泛型的协变性及通配符,以使得我们更好的更方便的来使用它
Covariance 协变 概念 在计算机科学中,具有继承关系的一对类型A、B,其中A≤B(即A继承于B或者说A是B的子类型),在类型构造器的映射下,分别变为类型A2、B2
若A2≤B2,即A2继承于B2或者说A2是B2的子类型,则称该类型构造器是协变 的
若A2≥B2,即B2继承于A2或者说B2是A2的子类型,则称该类型构造器是逆变 的
若上述两种均不适用,则称该类型构造器是不变 的
数组的协变性 这里我们先定义3个普通的POJO类,其中继承关系: BlackCat≤Cat≤Animal
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Data @AllArgsConstructor @NoArgsConstructor public class Animal { private String type; private String name; public String toString () { String str = "(type=" +getType()+",name=" +getName()+")" ; return str; } } ... @Data public class Cat extends Animal { public Cat (String type, String name) { super (type, name); } public String toString () { String str = "(type=" +getType()+",name=" +getName()+")" ; return str; } } ... @Data public class BlackCat extends Cat { public BlackCat (String type, String name) { super (type, name); } public String toString () { String str = "(type=" +getType()+",name=" +getName()+")" ; return str; } }
然后基于此,我们来通过下面测试方法来验证Java的数组是支持协变的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static void testArray () { Animal[] animalArray = new Cat [3 ]; System.out.println("Java 数组支持协变" ); Animal animal1 = new Animal ("Animal" ,"动物" ); Cat cat1 = new Cat ("Cat" , "猫" ); BlackCat blackCat1 = new BlackCat ("BlackCat" ,"黑猫" ); animalArray[1 ] = cat1; animalArray[2 ] = blackCat1; System.out.println( Arrays.toString(animalArray) ); animalArray[0 ] = animal1; System.out.println( Arrays.toString(animalArray) ); }
从其测试结果可以证明Java的数组是支持协变的 ,但由于animalArray数组的实际类型仍然是Cat类型,所以当我们将一个Animal类型的animal1赋值给该数组时,即会出现运行时异常。故Java中数组虽然支持协变,但却是不安全的
泛型的协变性 而对于泛型而言,其是不支持协变的 。同样地,我们来实际验证测试下泛型的协变性
1 2 3 4 5 6 7 8 public static void testGeneric () { LinkedList<BlackCat> blackCatList = new LinkedList <>(); LinkedList<Animal> animalList = blackCatList; }
可以看到,由于泛型不支持协变,故直接编译报错失败
通配符 上面通过实际的测试代码,我们验证明确了Java的泛型是不支持协变的,而泛型的通配符就是为了解决泛型不支持协变而引入的
上界通配符 extends 上界通配符使用 ? extends [TypeName] 表示,表明其可接收泛型类型是指定的类型及其子类型。这里我们通过一个例子来演示其用法与作用,如下所示。我们利用上界通配符定义了list1变量,其接收LinkedList的类型上界为Animal,所以catList可以被正确地赋值给它;变量list2同理。但是对于list1、list2而言,我们只能对其进行只读 操作。以list1变量为例进行说明,list1变量只知道其中的元素是Animal的子类但不知道具体的类型信息,故可以向上转型为Animal来读取。但是不可以向list1中添加元素等写操作。道理也很简单,list1不知道具体的元素类型,添加任意元素到list1中都是类型不安全的,这里list1中元素的实际类型为Cat,如果我们添加一个Animal类型的元素到其中,显然是错误的。故对于使用了上界通配符的变量来说,其只支持只读 操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static void testWildcard1 () { LinkedList<Cat> catList = new LinkedList <>(); Cat cat1 = new Cat ("Cat" , "猫1" ); Cat cat2 = new Cat ("Cat" , "猫2" ); Cat cat3 = new Cat ("Cat" , "猫3" ); catList.add(cat1); catList.add(cat2); catList.add(cat3); System.out.println("------ Demo1 ------" ); LinkedList<? extends Animal > list1 = catList; list1.forEach(System.out::println); System.out.println("------ Demo2 ------" ); LinkedList<? extends Cat > list2 = catList; list2.forEach(System.out::println); }
测试结果如下
下界通配符 super 下界通配符使用 ? super [TypeName] 表示,表明其可接收泛型类型是指定的类型及其父类型 。这里我们通过一个例子来演示其用法与作用,如下所示。我们利用下界通配符定义了list1变量,其接收LinkedList的类型下界为BlackCat,而catList类型为Cat是BlackCat父类,故可以被正确地赋值给它;变量list2同理,其可以接收一个泛型类型为Cat及其父类的List,而如果该List的范型类型为BlackCat,则显然不可接收。这就体现了super通配符的下界作用
对于list1、list2而言,我们只能对其进行只写 操作。以list1变量为例进行说明,因为我们知道list1所接收列表的范型类型,必然是[TypeName]及其父类型,这一点是由通配符super的下界来保证的。但是容易引起误解混淆的地方就在于,当我们向list1添加元素时,却只能添加[TypeName]及其子类型 ,即BlackCat及其子类的实例。咦?这里是不是感觉怪怪的?前面我们看到不是说接收类型只能是[TypeName]及其父类型么?怎么现在正好反过来了?请大家注意,前面我们说的是接收赋值操作,而现在我们说的是添加写入操作。需要注意进行区分!当向list1写入添加 BlackCat及其子类的实例 元素时,其必然可以向上转型为BlackCat类型,故可以成功进行添加。所以综上所述,对于下届通配符super而言,其限定类型[TypeName]在接收赋值时是类型的下界,而在写入添加元素时却是元素类型的上界。这样即可保证 添加时的元素类型 一定是 实际接收的列表的范型类型 的子类型,通过向上转型实现写入添加操作
而如果我们从list1进行读操作,由于无法确定元素的实际类型,故即使读出来了也只能由Object类型来接收。故对于使用了下界通配符的变量来说,其只支持只写 操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public static void testWildcard2 () { LinkedList<Cat> catList = new LinkedList <>(); Cat cat1 = new Cat ("Cat" , "猫1" ); Cat cat2 = new Cat ("Cat" , "猫2" ); Cat cat3 = new Cat ("Cat" , "猫3" ); catList.add(cat1); catList.add(cat2); catList.add(cat3); System.out.println("------ Demo1 ------" ); LinkedList<? super BlackCat> list1 = catList; BlackCat blackCat4 = new BlackCat ("BlackCat" , "黑猫4" ); list1.add(blackCat4); catList.forEach(System.out::println); System.out.println("------ Demo2 ------" ); LinkedList<? super Cat> list2 = catList; BlackCat blackCat5 = new BlackCat ("BlackCat" , "黑猫5" ); Cat cat6 = new Cat ("Cat" , "猫6" ); list2.add(blackCat5); list2.add(cat6); catList.forEach(System.out::println); System.out.println("------ Demo3 ------" ); Object o = list2.get(0 ); System.out.println("o: " + o); }
测试结果如下
无界通配符 ? 无界通配符使用 ? 表示,表明其可接收任意的泛型类型。这里我们通过一个例子来演示其用法与作用。比如我们期望提供一个打印List的方法printList。为使得该方法具备一定的通用性,故我们使用无界限定符。使得该方法的list形参可以接收任何泛型类型的List。显然使用无界通配符后,我们无法知道该list中元素的实际类型,故我们只能对该list进行只读 操作然后向上转型为Object类型进行处理,而不能进行写操作。聪明的朋友可能已经发现如果我们将无界通配符直接去掉,将形参list的类型由List<?>改为List同样可以满足需求啊?的确如此,但是使用无界通配符后可以保证如果我们不小心意外地修改了该list会出现编译期错误,以提醒我们及时修复提供了程序的健壮性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static void testWildcard3 () { System.out.println("------ Demo1 ------" ); List<Integer> list1 = new LinkedList <>(); Collections.addAll(list1, 1 ,2 ,3 ,4 ); printList(list1); System.out.println("------ Demo2 ------" ); List<Double> list2 = new LinkedList <>(); Collections.addAll(list2, 1.1 ,2.2 ,3.3 ); printList(list2); } private static void printList (List<?> list) { for (Object e : list) { System.out.println(e); } }
测试结果如下
Note
上界通配符、无界通配符只能读不能写,但是有一点例外,就是可以写入null
下界通配符只能写不能读,但是有一点例外,就是可以以Object类型读
PECS(Producer Extends, Consumer Super) 原则 PECS原则描述的是如何正确合理地应用泛型的上界通配符extends和下界通配符super。对于一个泛型容器、集合而言,如果该容器、集合的角色是作为一个Producer生产者,用来对外提供元素(即读操作)则应使用extends通配符;反之,如果该容器、集合的角色是作为一个Consumer消费者,用于接收外界提供的元素则应使用super通配符
在Java的集合API中大量应用了PECS原则。例如LinkedList的addAll方法作用是将集合c中的元素全部取出添加到主调者中,故集合c的角色是一个生产者,应使用extends上界通配符;而Collections的addAll方法则是将数组elements中的元素添加到集合c中,故集合c的角色是一个消费者,应使用super下界通配符
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class LinkedList <E> { public boolean addAll (Collection<? extends E> c) { return addAll(size, c); } ... } ... public class Collections { public static <T> boolean addAll (Collection<? super T> c, T... elements) { boolean result = false ; for (T element : elements) result |= c.add(element); return result; } ... }
下面即是上述规则的测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static void testPECS () { List<Integer> list1 = new LinkedList <>(); list1.add(1 ); list1.add(2 ); List<Integer> list2 = new LinkedList <>(); list2.addAll(list1); System.out.println("list2: " + list2); List<Integer> list3 = new LinkedList <>(); Collections.addAll(list3, 3 ,4 ); System.out.println( "list3: " + list3); }
测试结果如下
参考文献
Java核心技术·卷I 凯.S.霍斯特曼著