封面来源:碧蓝航线 铁血、音符 & 誓言 活动CG

本文参考:尚硅谷 宋红康 Java 零基础教程 P565-P576

其他参考链接:Java泛型(二) 协变与逆变

1. 为什么要使用泛型?

1.1 为什么要使用泛型

学习一个知识,得先明白这玩意能干嘛,为什么要用它。如果连这都不清楚,学习后又有什么用呢?

集合容器类在设计阶段 / 声明阶段不能确定这个容器到底实际存的是什么类型的对象,所以 在 JDK 1.5 之前只能把元素类型设计为 Object,JDK 1.5 之后使用泛型来解决这个问题。 因为这个时候除了元素的类型是不确定的,其他部分都是确定的,例如关于这个元素如何保存,如何管理等都是确定的,因此此时把元素的类型设计成一个参数,这个类型参数叫做范型。

Collection<E>List<E>ArrayList<E> 其中 <E> 就是类型参数,即泛型。

1.2 泛型的概念

所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。 这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量创建对象时)确定(即传入实际的类型参数,也称为类型实参)。

从 JDK 1.5 以后,Java引入了“参数化类型( Parameterized type)”的概念,允许我们在创建集合时再指定集合元素的类型,正如:List<String>, 这表明该 List 只能保存字符串类型的对象。

JDK 1.5 改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。

1.3 泛型有无的区别

说了这个多,还是没说为啥要有泛型,直接 Object 不是也可以存数据吗?

主要原因有两点:

1、解决元素存储的安全性问题,就好比商品、药品是不同的类型,就不会弄错

2、解决获取数据元素时,需要类型强制转换的问题,好比不用每回拿商品、药品都要辨别

在 JDK 1.5 以前使用集合时,由于没有引入泛型,则会:

当集合中没有泛型时

测试代码

在集合中没有使用泛型时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 在集合中未使用泛型时
@Test
public void test1() {
ArrayList list = new ArrayList();
// 需求:存放学生的成绩
list.add(78);
list.add(76);
list.add(89);
list.add(88);
// 问题一 类型不安全
list.add("Tom");
for (Object score : list) {
// 问题二 强转时,可能出现 ClassCastException
int stuScore = ((Integer) score);
System.out.println(stuScore);
}
}

在集合中使用泛型,且使用迭代器进行遍历:

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
44
45
// 在集合中使用泛型
@Test
public void test2() {
ArrayList<Integer> list = new ArrayList<>();
list.add(78);
list.add(76);
list.add(89);
list.add(88);
// 编译时就会进行类型检查,保证数据的安全
// list.add("tom");
// 方式一
for (Integer score : list) {
// 避免了强制类型转换
int stuScore = score;
System.out.println(stuScore);
}

// 方式二 迭代器
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer stuScore = iterator.next();
System.out.println(stuScore);
}
}

// 在 HashMap 中使用泛型 迭代器进行遍历
@Test
public void test3() {
// HashMap<String, Integer> map = new HashMap<String, Integer>();
// JDK 7 新特性:类型推断
HashMap<String, Integer> map = new HashMap<>();
map.put("Tom", 87);
map.put("Jerry", 87);
map.put("Jack", 67);

Set<Map.Entry<String, Integer>> entries = map.entrySet();
Iterator<Map.Entry<String, Integer>> iterator = entries.iterator();

while (iterator.hasNext()) {
Map.Entry<String, Integer> next = iterator.next();
String key = next.getKey();
Integer value = next.getValue();
System.out.println(key + " ----> " + value);
}
}

需要注意的是: 泛型的类型必须是类,不能是基本数据类型,需要用到基本数据类型的位置,应该使用那个基本数据类型的包装类。

总的来说,使用泛型后有以下两个优点:

1、编译时就会进行类型检查,保证数据的安全

2、避免了强制类型转换

2. 自定义泛型

2.1 自定义泛型类

自定义泛型类:

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
package com.yang.generic;

/**
* @author 默烦 2020/10/9
*/
// 自定义泛型类
public class Order<T> {

String orderName;
int orderId;

// 类的内部结构就可以使用类的泛型

T orderT;

public Order() {
}

public Order(String orderName, int orderId, T orderT) {
this.orderName = orderName;
this.orderId = orderId;
this.orderT = orderT;
}

public T getOrderT() {
return orderT;
}

public void setOrderT(T orderT) {
this.orderT = orderT;
}

@Override
public String toString() {
return "Order{" +
"orderName='" + orderName + '\'' +
", orderId=" + orderId +
", orderT=" + orderT +
'}';
}
}

继承了自定义泛型类的两个子类。

不再是一个泛型类:

1
2
3
4
5
6
7
/**
* @author 默烦 2020/10/9
*
* 不再是一个泛型类
*/
public class SubOrder extends Order<Integer>{
}

仍然是一个泛型类:

1
2
3
4
5
6
7
/**
* @author 默烦 2020/10/9
*
* 仍然是一个泛型类
*/
public class SubOrder1<T> extends Order<T>{
}

测试代码:

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
package com.yang.generic;

import org.junit.Test;

/**
* @author 默烦 2020/10/9
* <p>
* 自定义泛型类
*/
public class GenericTest1 {
@Test
public void test1() {
// 如果定义了泛型类,实例化没有指明类的范型,则认为泛型类型为 Object 类型
// 要求:如果自定义了范型类,实例化时需要指明类的泛型
Order order = new Order();
order.setOrderT(123);
order.setOrderT("ABC");

// 实例化时指明类的泛型
Order<String> order1 = new Order<>("orderAA", 1001, "order:AA");
order1.setOrderT("AA:hello");

}

@Test
public void test2() {
SubOrder subOrder = new SubOrder();
// 由于子类在继承带泛型的父类时,指明了范型类型,则实例化子类对象时,不需要再指明泛型
subOrder.setOrderT(123);

SubOrder1<String> subOrder1 = new SubOrder1<>();
subOrder1.setOrderT("subOrder1...");
}
}

如果子类在继承带泛型的父类时,指明了范型类型,则实例化子类对象时,不需要再指明泛型。

2.2 要点重点

1、泛型类可能有多个参数,此时应当将多个参数一起放在尖括号里。比如:<E1, E2, E3>

2、泛型类的构造器如下:public GenericClass(){} ,而不是 public GenericClass<E>(){}

3、实例化后,操作原来泛型的位置的结构必须与指定的泛型类型一致。

4、泛型不同的应用不能相互赋值,道理很简单,类型都不一样,赋值个 🔨 。

  • 尽管在编译时 ArrayList<String>ArrayList<Integer> 是两种类型,但是在运行时只有一个 ArrayList 被加载到 JVM中。

5、泛型如果不指定,将被擦除,泛型对应的类型均按照 Object 处理,但不等价于 Object

  • 经验之谈: 泛型要使用就一路使用,不使用就一直别使用。

6、如果泛型类是一个接口或抽象类,则不可创建泛型类的对象。

7、JDK 1.7 后,有一个泛型简化操作:ArrayList<String> list = new ArrayList<>();

8、泛型的指定中不能使用基本数据类型,需要使用对应的包装类进行替代。

9、在类 / 接口上声明的泛型,在本类或本接口中即代表某种类型,可以作为非静态属性的类型、非静态方法的参数类型、非静态方法的返回值类型。注意: 在静态方法中不能使用类的泛型。

10、异常类不能是泛型的。

11、不能使用 new E[] ,原因也很简单,此时的 E 还是相当于一个变量,而使用 new 关键字时,需要保证类是指明的,但是可以:E[] elements = (E[])new Object[capacity];

  • 参考:ArrayList 源码中 声明:Object[] elementData,而非泛型参数类型数组。

12、父类有泛型,子类可以选择保留泛型也可以选择指定泛型类型:

  • 子类不保留父类的泛型:按需实现

    • 没有类型,擦除
    • 具体类型
  • 子类保留父类的泛型:泛型子类

    • 全部保留
    • 部分保留

结论:子类必须是“富二代”,子类除了指定或保留父类的泛型,还可以增加自己的泛型。

2.3 自定义泛型方法

不是说方法中使用了类的泛型就叫做泛型方法,以下方法都不是泛型方法:

1
2
3
4
5
6
7
8
9
10
11
12
public class Order<T> {
T orderT;

// 以下方法都不是泛型方法
public T getOrderT() {
return orderT;
}

public void setOrderT(T orderT) {
this.orderT = orderT;
}
}

泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系。换句话说,泛型方法所属的类是不是泛型类没有关系。

泛型方法示例:

1
2
3
4
5
6
7
8
9
10
11
12
public class Order<T> {
// 省略其他代码

// 泛型方法
public <E> List<E> copyFromArrayToList(E[] array) {
ArrayList<E> list = new ArrayList<>();
for (E e : array) {
list.add(e);
}
return list;
}
}

测试代码:

1
2
3
4
5
6
7
8
9
10
// 泛型方法测试
@Test
public void test4() {
Order<String> order = new Order<>();
Integer[] array = new Integer[]{1, 2, 3, 4};
// 泛型方法在调用时,指明泛型参数的类型
List<Integer> list = order.copyFromArrayToList(array);

System.out.println(list);
}

需要注意的是: 在静态方法中不能使用类的泛型,但泛型方法是可以声明为静态的。 原因也很简单,泛型参数是在调用方法时确定的,并非在实例化类时确定。

2.4 使用场景

在实际业务的数据访问层中,会涉及到大量的增删改查,我们可以将一些简单的增删改查提取出来,并将其设置成泛型类,如:

1
2
3
4
5
6
7
8
9
10
11
public class DAO<T> {
// 添加方法
public void add(T t) {

}
// 删除方法
public boolean remove(int index) {

}
// 省略其他代码
}

这些简单的增删改查是可以复用的,基本上每个 DAO 中都有这些方法,因此将它们提取出来,后续使用的时候直接继承这个类就可以了。如:

1
2
3
public class StudenDao extends DAO<Student> {

}

3. 继承与泛型

假设 类A 是类 B 的父类,那么 G<A>G<B> 有什么关系?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 类 A 是类 B 的父类,G<A> 与 G<B> 无子父类关系,二者是并列关系
@Test
public void test1() {
Object obj = null;
String str = null;
obj = str;

Object[] arr1 = null;
String[] arr2 = null;
arr1 = arr2;

List<Object> list1 = null;
List<String> list2 = null;
// 此时 list1 与 list2 的类型不具备子父类关系
list1 = list2; // 报错,编译不通过
}

上述代码在编译器中直接报错,编译不通过,证明是不能那么写的。

但是现在又有另外一种情况,类 A 是类 B 的父类,A<G>B<G> 又有什么关系呢?

1
2
3
4
5
6
7
8
9
10
11
// 类 A 是类 B 的父类,A<G> 是 B<G> 的父类
@Test
public void test2() {
AbstractList<String> list1 = null;
List<String> list2 = null;
ArrayList<String> list3 = null;

// 以下情况并不会报错
list1 = list3;
list2 = list3;
}

简单!👍

4. 通配符

4.1 简单使用

上文说到:类A 是类 B 的父类, G<A>G<B> 是没有子父类关系的,但是它俩还是有一点关系的。这里涉及到通配符 ? 的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 通配符 ? 的使用
// 类 A 是类 B 的父类,G<A> 与 G<B> 是没有关系的,两者共同的父类是 G<?>
@Test
public void test3() {
List<Object> list1 = null;
List<String> list2 = null;

List<?> list = null;

// 以下代码不会报错
list = list1;
list = list2;

print(list1);
print(list2);
}

public void print(List<?> list) {
Iterator<?> iterator = list.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
}

4.2 写入与读取

在使用了通配符 ? 后,可以进行写入和读取数据吗?

在 4.1 的测试代码中进行修改,最终得到以下代码:

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
 @Test
public void test3() {
List<Object> list1 = null;
List<String> list2 = null;

List<?> list = null;

list = list1;
list = list2;

// print(list1);
// print(list2);

List<String> list3 = new ArrayList<>();
list3.add("AA");
list3.add("BB");
list3.add("CC");
list = list3;
// 添加:对于 List<?> 就不能向其内部添加数据了
// 除了添加 null
list.add("DD"); // 报错
list.add(null); // 这不会报错

// 获取(读取):允许读取,读取的数据类型是 Object
Object o = list.get(0);
System.out.println(o); // 输出 AA
}

总结一下,对于 List<?> 不能向其内部添加数据(除了 null,但是并没有什么用,谁没事添加这玩意),但可以读取其中的数据,读取的数据类型是 Object

4.3 存在限制条件

在有些代码中,我们可以看到以下类似的写法:

1
2
List<? extends Person> list1 = null; 
List<? super Person> list2 = null;

这种写法就是一种存在限制条件的泛型写法。那么这种写法与原来的写法有什么区别呢?

现有以下两个类,其中 Student 类是 Person 类的子类:

1
2
3
4
5
public class Person {
}

public class Student extends Person{
}

然后我们可以编写一个测试代码来测试一下:

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
/*
* 有限制条件的通配符的使用
* ? extends A :
* G<? extends A> 可以作为 G<A> 和 G<B> 的父类,其中 B 是 A 的子类
* ? super B :
* G<? super A> 可以作为 G<A> 和 G<B> 的父类,其中 B 是 A 的父类
* */
@Test
public void test4() {
List<? extends Person> list1 = null; // 类型范围小于等于 Person
List<? super Person> list2 = null; // 类型范围大于等于 Person

List<Student> list3 = null;
List<Person> list4 = null;
List<Object> list5 = null;

list1 = list3;
list1 = list4;
// list1 = list5; // 报错

// list2 = list3; // 报错
list2 = list4;
list2 = list5;

// 读取数据
list1 = list4;
Person person = list1.get(0);
// Student student = list1.get(0); // 报错
list2 = list4;
Object object = list2.get(0);
// Person p = list2.get(0); // 报错

// 写入数据
// list1.add(new Student()); // 报错

list2.add(new Person());
list2.add(new Student());
}

有些地方会报错的原因听我细细道来:

1、list1 = list5 会报错,是因为:list1 的范围应当小于等于 Person,而 list5 的范围是 Object

2、list2 = list3 会报错,是因为:list2 的范围应当大于等于 Person,而 list3 的范围是 Student

3、Student student = list1.get(0); 会报错,是因为:list1 的范围应当小于等于 Person,但也有可能比 Student 的范围更小

4、Person p = list2.get(0); 会报错,是因为:list2 的范围应当大于等于 Person,但也有可能比 Person 更大

5、list1.add(new Student()); 会报错,是因为:list1 的范围应当小于等于 Person,但也有可能比 Student 的范围更小

6、list2.add(new Student()); 不会报错,是因为:list2 的范围应当大于等于 Person,写 Person 类及其子类当然是可以的,但是不能写范围比 Person 大的类(不能写 Person 的父类)。如果我初始化的类型是 A(A 是 Person 的父类),但是又 add 了 B(B 也是 Person 的父类),两个类型不一样,肯定是不能 add Person 的父类。

5. 协变与逆变

5.1 泛型是不变的

一个问题,下面的代码会编译报错吗?

1
List<Object> list = new ArrayList<String>();

会编译报错!

为什么呢?

假设不会编译报错,那么我们就可以进行以下操作:

1
2
3
List<Object> list = new ArrayList<String>();
list.add(new Person());
String str = list.get(0);

很显然,这是不对的。像上述代码一样,无法保证 get 出来的数据一定是 String 类型的,因此 Java 将泛型设置为不变的。

也就是说,List<Object>ArrayList<String> 之间是没有关系的,他们并不等价。

从上述示例也可以看出,Java 的泛型没有 内建的协变类型,无法将 List<Object>ArrayList<String> 关联起来。当然了,也没有 内建的逆变类型

需要额外提一句,Java 中数组是有内建的协变类型,比如下面这样是不会编译报错的:

1
2
3
Number[] array = {new Integer(1), new Double(1.2), new Float(1.23)};
// 当然拆下箱更好
Number[] array = {1, 1.2, (float) 1.23};

假设又有这样一段代码:

1
2
3
Number[] array = new Integer[10];
array[0] = 1;
array[1] = 1.2; // java.lang.ArrayStoreException: java.lang.Double

尽管编译时不会报错,但在运行时处理的是 Integer[],因此在向数组中放置异构类型时会抛出异常。

但有些情况下可能需要将一个 new ArrayList<String> 转换为 List<Object>,应该咋办呢?

5.2 泛型的协变

从【4. 通配符】中可知,在泛型中可以使用 ? 通配符,并且可以搭配 extendssuper 使用,利用通配符就可以实现协变和逆变。

协变与逆变的定义

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

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

协变

先看一个例子:

1
2
3
4
5
6
7
public static Double sum(List<Number> list) {
double sum = 0;
for (Number number : list) {
sum += number.doubleValue();
}
return sum;
}

很明显,这是对一个数字列表进行求和。现在我想这么去调用:

1
2
3
4
List<Double> list = new ArrayList<>();
list.add(1.2);
list.add(1.8);
Double sum = sum(list);

乍眼一看,没啥问题。但由于 泛型是不变的,因此这里调用 sum(list) 会编译报错。

我们希望传入 sum() 方法的参数是一个 List 列表,并且其内部的元素是 Number 的子类。说到子类,难免会想到 extends 关键词,因此这里可以使用通配符 ? extends 结合,实现 协变

1
2
3
public static Double sum(List<? extends Number> list) {
// 具体实现
}

类似 <? extends T> 被称为 子类通配符,意味着这个泛型可以匹配 T 及其子类。

这样做也会带来一定的代价:无法向集合中再添加元素了。

List<? extends Number> 为例,它可以合法地指向 List<Double>,这时向集合内添加诸如 IntegerFloat 等类型的数据显然是不合法的。除此之外,List<? extends Number> 还可以合法地指向 List<Integer>List<Float> 等集合,具体指向什么只有在调用方法的时候才知道,也就是说编译器不知道 List<? extends Number> 的具体类型是什么,因此一旦使用了这种向上转型,将丢失向集合内添加元素的能力。

假设 Number 是非抽象类型的父类,针对 List<? extends Number> 而言,向这个集合中添加 Number 类型的元素也是不行的,因为编译器不知道指向的具体类型是什么,同时泛型还是不变的。

简单来说,使用 <? extends T> 后,编译器 只知道类型的上界T,而无法知道下界是什么,因此也就无法向集合内添加元素了。

也不是说不能添加任何元素,null 还是可以的,只不过没人会那么做。

5.3 泛型的逆变

前面已经说过,下面这段代码是会编译报错的:

1
2
3
List<Object> list = new ArrayList<String>();
list.add(new Person());
String str = list.get(0);

利用 协变,可以只调用 get() 方法,而不能调用 add() 方法。

那我就想调用 add() 方法怎么办呢?

先看 JDK8 ArrayList 中新增的一个方法:

1
2
3
public boolean removeIf(Predicate<? super E> filter) {
// 具体实现
}

这个方法表示传入一个过滤器 Predicate,删除当前集合中符合过滤器条件的数据。

过滤器 Predicate 的泛型是 <? super E>,类似 <? super E> 被称为 超类通配符,利用超类通配符实现了范型的 逆变。假设这个方法没有使用逆变,而是:

1
2
3
public boolean removeIf(Predicate<E> filter) {
// 具体实现
}

假设当我们需要删除 Double 类型的集合中大于 0 的元素时,可以这么写:

1
2
3
4
5
6
7
8
9
10
ArrayList<Double> doubleList = new ArrayList<>();

Predicate<Double> doublePredicate = new Predicate<Double>() {
@Override
public boolean test(Double num) {
return num > 0;
}
};

doubleList.removeIf(doublePredicate);

又假设需要删除 Integer 类型的集合中大于 0 的元素,就要这么写:

1
2
3
4
5
6
7
8
9
ArrayList<Integer> integerList = new ArrayList<>();

Predicate<Integer> integerPredicate = new Predicate<Integer>() {
@Override
public boolean test(Integer integer) {
return integer > 0;
}
};
integerList.removeIf(integerPredicate);

由于泛型是不变的,如果想要复用 doublePredicate 也是不行的,像下面这样就会编译报错:

1
2
3
4
5
6
7
8
9
10
Predicate<Double> doublePredicate = new Predicate<Double>() {
@Override
public boolean test(Double num) {
return num > 0;
}
};

ArrayList<Integer> integerList = new ArrayList<>();

integerList.removeIf(doublePredicate); // 编译报错

我们知道 DoubleInteger 都是 Number 的子类,如果可以将 Predicate 的泛型设置为 Number 类型,那不就可以对 DoubleInteger 类型的集合复用了。

想法是很好的,同样由于泛型是不变的,在没有使用逆变的情况下,直接传入 Number 类型的 Predicate 也还是会报错。正因如此,removeIf() 方法的参数使用泛型的逆变,被设计为 Predicate<? super E>,而不是 Predicate<E>

在使用了逆变的情况下,就可以这样操作 DoubleInteger 类型的集合:

1
2
3
4
5
6
7
8
9
10
11
Predicate<Number> predicate = new Predicate<Number>() {
@Override
public boolean test(Number number) {
return number.doubleValue() > 0;
}
};
ArrayList<Double> doubleList = new ArrayList<>();
doubleList.removeIf(predicate);

ArrayList<Integer> integerList = new ArrayList<>();
integerList.removeIf(predicate);

逆变也是有代价的,使用了逆变可以 确定类型的下界,而无法确定上界(与协变相反),因此使用了逆变将丧失获取该类型的能力。道理很简单,编译器不知道给定下界和 Object(所有类型都是 Object 类型的子类)类型之间到底有怎样的继承关系,为了防止由于“泛型是不变的”带来的编译报错,编译器干脆就不允许在使用逆变后再获取该类型。

假设有一 Student 类,其父类为 Person。利用这两个类型来看看使用了逆变后的得与失:

1
2
3
4
5
6
7
8
public void test(List<? super Student> list) {
Object object = list.get(0); // 没问题
Student student = list.get(0); // 编译报错
Person person = list.get(0); // 编译报错

list.add(new Student());
list.add(new Person()); // 编译报错
}

可以看到使用逆变后,就无法从集合中再获取原类型了。当然,可以获取 Object 类。

除此之外,使用逆变后可以消费泛型。以上述为例,就是可以向集合内添加元素,但并不是任何元素都是可行的,只能添加给定类型及其子类的元素。原因同样是只确定了下界而没有确定上界。

5.4 PECS 原则

那什么时候使用协变,什么时候使用逆变呢?

《Effective Java》给出了一个原则:producer-extends, consumer-super(PECS)

当需要生产一个泛型时(从泛型类获取指定类型的数据,并且不需要写入),可以使用 extends,即协变。比如从集合中获取指定下标的元素。

当需要消费一个泛型时(需要向泛型类写入指定类型的数据,但不需要获取这种类型),可以使用 super,即逆变。比如在集合中按照指定条件删除元素。

但如果又要写入数据,又要获取数据,就不能使用通配符 ?,而是指定具体的泛型。

java.util.Collections 中有一个 copy() 方法:

1
2
3
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
// 具体实现
}

copy() 方法是将源列表中的元素拷贝到目标列表中,并且目标列表中每个复制元素的索引将与其在源列表中的索引相同。因此,目标列表必须至少与源列表一样长,如果更长,则目标列表中的其余元素不受影响。

显然是需要从 src 中获取需要拷贝的元素,即生产泛型,然后将获取到的元素放到 dest 中指定的下标位置,即消费泛型。因此 src 使用 extends,而 dest 使用 super

6. 自限定类型

6.1 普通泛型类构成自限定

在使用 Java 泛型时经常会看到下述这样的“迷惑”写法:

1
public class SelfBound<T extends SelfBound<T>> { }

SelfBound 类的类型参数是 TT 由一个边界类限定,这个边界恰好又是参数类型为 TSelfBound,似乎变成了无限循环。这种写法被称为自限定类型(Self-Bound Types)。

看一个例子,使用普通泛型类构成自限定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BasicHolder<T> {
T element;

void set(T arg) {
element = arg;
}

T get() {
return element;
}

void f() {
System.out.println(element.getClass().getSimpleName());
}
}
1
2
public class Subtype extends BasicHolder<Subtype>{
}
1
2
3
4
5
6
7
8
9
10
public class SimpleTest {
public static void main(String[] args) {
Subtype st1 = new Subtype(), st2 = new Subtype(), st3 = new Subtype();
st2.set(st3);
st1.set(st2);
Subtype st4 = st1.get();
st4.f(); // Subtype
Subtype st5 = st1.get().get(); // 链式调用
}
}

Subtype 类继承了 BasicHolder 类的属性与方法,element 属性的类型具有 Subtype 类型,而不仅仅是 BasicHolder

上述示例代码中,BasicHolder 类变成了其所有子类的公共模板,对于子类所继承的属性与方法将使用准确的类型而不是基类。

class Subtype extends BasicHolder<Subtype> 这样就构成了 自限定SubtypeBasicHolder 所继承来的 element 属性、set() 方法的参数、get() 方法的返回值都是 Subtype,而不是 BasicHolder,这样 Subtype 对象只允许与 SubType 对象进行交互,不允许其与 BasicHolder 的其他子类对象进行交互。

也就是说,自限定类型定义了一个基类,这个基类能够使用子类作为其参数、返回值类型、属性类型。

6.2 自限定与协变

使用自限定类型可以产生 协变参数类型,即:方法参数类型跟随子类而变化。当然,也可以产生 协变返回类型

协变返回类型

不使用泛型时,子类重写基类的方法,返回更确切的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base {}
class Derived extends Base {}

interface OrdinaryGetter {
Base get();
}

interface DerivedGetter extends OrdinaryGetter {
Derived get();
}

public class CovariantReturnTypes {
public void test1(DerivedGetter derivedGetter) {
Derived derived = derivedGetter.get();
// 当然这样也是可以的
Base base = derivedGetter.get();
}
}

使用泛型后,子类继承具有自定义类型的基类,子类所继承的方法将返回更确切的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface GenericsGetter<T extends GenericsGetter<T>> {
T get();
}

interface Getter extends GenericsGetter<Getter> {
}

public class GenericsAndReturnTypes {
public void test2(Getter g) {
Getter result = g.get();
// 这样也是可以的
GenericsGetter<Getter> genericsGetter = g.get();
}
}

协变参数类型

在不使用泛型时,基类方法参数不能随着子类的类型发生变化。 方法只能重载不能重写。

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class OrdinarySetter {
void set(Base base) {
System.out.println("OrdinarySetter.set(Base)");
}
}

class DerivedSetter extends OrdinarySetter {
void set(Derived derived) {
System.out.println("DerivedSetter.set(Derived)");
}
}

public class OrdinaryArguments {
public static void main(String[] args) {
Base base = new Base();
Derived derived = new Derived();
DerivedSetter ds = new DerivedSetter();
// DerivedSetter.set(Derived)
ds.set(derived);
// OrdinarySetter.set(Base)
ds.set(base);
}
}

DerivedSetter 子类使用 basederived 作为参数调用 set() 方法后,并不是只调用重写的方法,也调用了其基类的方法,从这里也可以论证方法只能重载而不能重写。

但在使用自定义限定类型后,子类所继承的方法不再以基类类型为参数,而是接受其本身作为参数。比如:

1
2
3
4
5
6
7
8
9
10
11
12
interface SelfBoundSetter<T extends SelfBoundSetter<T>> {
void set(T args);
}

interface Setter extends SelfBoundSetter<Setter> {}

public class SelfBoundAndCovariantArguments {
public void testA(Setter s1, Setter s2, SelfBoundSetter sbs) {
s1.set(s2);
// s1.set(sbs); // 编译错误
}
}

6.3 自限定类型的应用

我们知道在 JDK5 中引入了枚举,使用 enum 关键词就可以创建枚举类。这其实是一个语法糖,使用 enum 关键词后就相当于继承了 java.lang.Enum 基类。

JDK 中 Enum 是这样定义的:

1
2
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable { }

很明显,Enum 的定义使用了自限定类型。

那为什么要这么声明呢?像这样不行吗?

1
public abstract class Enum implements Comparable<E>, Serializable { }

因为 Enum 是一个抽象类,我们使用的都是它的子类,比如:

1
class Month extends Enum { }
1
class Week extends Enum { }

我们会在 Month 中定义从一月到十二月共十二个枚举,在 Week 中定义从星期一到星期天共七个枚举。

Enum 实现了 Comparable 接口,因此枚举都是可比较的。比如可以在 Month 中比较一月和二月哪个大,也可以在 Week 中比较星期一和星期天哪个大,但是能比较 Month 中的一月和 Week 中的星期一哪个大吗?

如果使用自定义的 Enum 抽象类,那么这是可以比较的,但这显然不是我们所期望的。我们期望是:同一个 Enum 子类的实例进行比较,而不是两个不同的 Enum 子类实例进行比较,因为这是没有意义的。

因此 JDK 中 Enum 的定义使用了自限定类型,保证其子类所继承的 compareTo() 方法的参数是其本身。

总结下 JDK Enum 的设计思路

1、首先枚举是可以比较的,因此 Enum 没有定义成:

1
public abstract class Enum extends Object { }

2、我们希望枚举在比较时只能和枚举进行比较,而不是和任何类型,因此 Enum 没有定义成:

1
public abstract class Enum<E> implements Comparable<E>, Serializable { }

3、我们还希望只有同一个枚举类之间才能比较,而不是随便两个枚举类之间就可以比较,因此在 Enum 的定义中使用自类型限定。最终,Enum 就长这样:

1
2
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable { }

6.4 为什么没有编译报错?

现有如下类和方法:

1
2
3
4
5
6
7
static class Obj {
}

private <T, U, R> void func(T obj,
Function<T, U> fun1,
Function<R, U> fun2) {
}

以下使用方式不会编译报错:

1
2
3
Obj obj = new Obj();
func(obj, Object::hashCode, Obj::hashCode);
func(obj, Obj::toString, Object::toString);

那么这样的使用会编译报错吗?

1
func(obj, Obj::hashCode, Obj::toString);

答案是也不会。

hashCode() 方法的返回值类型是 inttoString() 方法的返回值类型是 Stringfunc() 方法的第二个和第三个 Function 类型的参数的第二个泛型参数都是 U,表示方法的出参类型应该一致,但一个是 int,一个是 String,显然不一样,为什么不会编译报错呢?

这是因为在泛型推断时,将 U 推断为 IntegerString 的相同父类 Comparable,因此不会编译报错。

由于所有类的“祖宗类”都是 Object,因此在不加任何限定的情况下,func() 方法的第二个和第三个参数可以任意填写,比如一个返回 HashMap,一个返回 HashSet 都不会编译报错:

1
func(obj, obj1 -> new HashMap<>(), o -> new HashSet<>());

再回到最开始,如何使 func(obj, Obj::hashCode, Obj::toString); 的写法产生编译报错呢?

也就是说,int 类型只能和 int 类型比较,String 类型只能和 String 类型比较,根据本节的知识,很容易想到让泛型参数 U 构成自限定:

1
2
3
4
private <T, U extends Comparable<U>, R> void func(T obj,
Function<T, U> fun1,
Function<R, U> fun2) {
}

7. 捕获转换

7.1 无界通配符

<? extends T> 被称为 子类通配符,它确定了类型的上界;<? super E> 被称为 超类通配符,它确定了类型的下界;而 <?> 被称为 无界通配符,它并没有确定类型的上界和下界。

可以在下面这两种场景下使用无界通配符:

  1. 正在编写一个可以使用 Object 类中提供的功能来实现的方法(所有类的父类都是 Object);
  2. 代码在泛型中使用不依赖类型参数的方法。比如经常使用 Class<?> 就是因为类 Class<T> 中的大多数方法不依赖并不于 T

注意 List<?>List<Object> 是不一样的。可以将 Object 类型或及其子类对象插入到 List<Object>,但是对于 List<?> 就只能插入 null。在调用 get() 获取元素时,它们调用 get() 方法的返回值都是 Object

List<?> 表示的是“具有某种特定类型的 List,但这个类型是未知的”,因此可以将任何类型的 List 赋值给 List<?>,对于 List<Object> 则不是这样。

7.2 捕获转换

通常情况下,使用原生类型和 <?> 没什么区别,但是使用 <?> 可以实现捕获转换。

捕获转换(Capture conversion)允许编译器为已捕获的通配符产生一个占位符类型名,以便对它进行类型推断。

直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Holder<T> {
private T value;

public Holder() {
}

public Holder(T val) {
value = val;
}

public void set(T val) {
value = val;
}

public T get() {
return value;
}
}
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
public class CaptureConversion {

public static <T> void f1(Holder<T> holder) {
T t = holder.get();
System.out.println(t.getClass().getSimpleName());
}

public static void f2(Holder<?> holder) {
f1(holder);
}


public static void main(String[] args) {
Holder raw = new Holder<Integer>(1);
// Unchecked assignment: 'Holder' to 'Holder<java.lang.Object>'
f1(raw);
f2(raw);

Holder rawBasic = new Holder();
// Unchecked call to 'set(T)' as a member of raw type 'Holder'
rawBasic.set(new Object());
f2(rawBasic);

Holder<?> wildcards = new Holder<Double>(1.0);
f2(wildcards);
}
}

示例代码中调用 f1() 时 IDEA 会进行警告,而调用 f2() 时却没有,这是因为 f2() 的 holder 参数可以捕获到原生类型中的参数类型,但自己却不知道,捕获到的参数类型可以转换成 f1() 中的确切类型。

捕获转换只适用在方法内部,并且需要传入确切的类型。需要注意的是,不能从 f2() 中返回 T,因为 T 对于 f2() 来说是未知的。