9.4.5 Java8改进的类型推断

Java8改进了泛型方法的类型推断能力,类型推断主要有如下两方面。

  • 可通过调用方法的上下文来推断泛型的目标类型。
  • 可在方法调用链中,将推断得到的泛型传递到最后一个方法。

如下程序示范了Java8对泛型方法的类型推断。

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
class MyUtil<E> {
public static <Z> MyUtil<Z> nil() {
return null;
}

public static <Z> MyUtil<Z> cons(Z head, MyUtil<Z> tail) {
return null;
}

E head() {
return null;
}
}

public class InferenceTest {
public static void main(String[] args) {
// 可以通过方法赋值的 目标参数 来推断类型参数为String
MyUtil<String> ls = MyUtil.nil();//1

// 无需使用下面语句在调用nil()方法时指定 类型参数 的类型
MyUtil<String> mu = MyUtil.<String>nil();//2

// 可调用cons方法所需的参数类型来推断类型参数为Integer
MyUtil.cons(42, MyUtil.nil());//3
// 无需使用下面语句在调用nil()方法时指定类型参数的类型
MyUtil.cons(42, MyUtil.<Integer>nil());//4

// 希望系统能推断出调用nil()方法类型参数为String类型,
// 但实际上Java 8依然推断不出来,所以下面代码报错
// String s = MyUtil.nil().head();
String s = MyUtil.<String>nil().head();
}
}

上面程序中代码1和代码2的作用完全相同,但第代码1无须在调用 MyUtil类的nil方法时显式指定泛型参数为 String,这是因为程序需要将该方法的返回值赋值给 MyUtil<String>类型,因此系统可以自动推断出此处的泛型参数为String类型。

上面程序代码3和代码4的作用也完全相同,但代码3也无须在调用yul类的nil方法时显式指定泛型参数为 Integer,这是因为程序将nil方法的返回值作为了 MyUtil类的 cons()方法的第二个参数,而程序可以根据cons()方法的第一个参数(42)推断出此处的泛型参数为 Integer类型。

需要指出的是,虽然Java8增强了泛型推断的能力,但泛型推断不是万能的,例如如下代码就是错误的。

1
2
3
// 希望系统能推断出调用nil()方法类型参数为String类型,
// 但实际上Java 8依然推断不出来,所以下面代码报错
String s = MyUtil.nil().head();

因此,上面这行代码必须显式指定泛型的实际类型,即将代码改为如下形式

1
String s = MyUtil.<String>nil().head();

9.4.4 泛型方法与方法重载

因为泛型既允许设定通配符的上限,也允许设定通配符的下限,从而允许在一个类里包含如下两个方法定义:

1
2
3
4
5
6
7
8
public class MyUtils{
public static <T> void copy(Collection<T> dest Collection<? extends T> src){
...
}// 1号代码
public static <T> T copy (Collection<? super T> dest, Collection<T> src){
...
}//2号代码
}

上面的MyUtils类中包含两个copy方法,这两个方法的参数列表存在一定的区别,但这种区别不是很明确:这两个方法的两个参数都是Collection对象,前一个集合里的集合元素类型是后一个集合里集合元素类型的父类。如果只是在该类中定义这两个方法不会有任何错误,但只要调用这个方法就会引起编译错误。例如,对于如下代码:

1
2
3
List<Number> ln =new ArrayList<>();
List<Integer> li = new ArrayList<>();
copy(ln,li);//3号代码

上面程序中3号代码部分调用copy方法,但这个copy方法既可以匹配1号copy方法,此时泛型T表示的类型是Number;也可以匹配2号copy方法,此时泛型T表示的类型是Integer。编译器无法确定这行代码想调用哪个copy方法,所以这行代码将引起编译错误。

9.4.3 Java7的“菱形”语法与泛型构造器

正如泛型方法允许在方法签名中声明泛型形参一样,Java也允许在构造器签名中声明泛型形参,这样就产生了所谓的泛型构造器。

一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让Java根据数据参数的类型来“推断”泛型形参的类型,而且程序员也可以显式地为构造器中的泛型形参指定实际的类型。如下程序所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Foo {
// 访问控制符 泛型形参 构造器名 参数列表
public <T> Foo(T t) {
System.out.println(t);
}
}

public class GenericConstructor {
public static void main(String[] args) {
// 泛型构造器中的T类型为String。
new Foo("疯狂Java讲义");
// 泛型构造器中的T类型为Integer。
new Foo(200);

// 显式指定泛型构造器中的T类型为String,
// 传给Foo构造器的实参也是String对象,完全正确。
new <String>Foo("疯狂Android讲义"); //1号代码

// 显式指定泛型构造器中的T类型为String,
// 但传给Foo构造器的实参是Double对象,下面代码出错
// new <String> Foo(12.3);//2号代码
}
}

上面程序中1号代码不仅显式指定了泛型构造器中的泛型形参T的类型应该是String,而且程序传给该构造器的参数值也是String类型,因此程序完全正常。
但在2号代码 处,程序显式指定了泛型构造器中的泛型形参T的类型应该是String,但实际传给该构造器的参数值是Double类型,因此这行代码将会出现错误。

显式指定了泛型构造器中声明的泛型形参的实际类型,则不可以使用“菱形”语法

前面介绍过Java7新增的“菱形”语法,它允许调用构造器时在构造器后使用一对尖括号来代表泛型信息。但如果程序显式指定了泛型构造器中声明的泛型形参的实际类型,则不可以使用“菱形”语法。如下程序所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyClass<E> {
// 访问控制符 泛型形参 构造器名 参数列表
public <T> MyClass(T t) {
System.out.println("t参数的值为:" + t);
}
}

public class GenericDiamondTest {
public static void main(String[] args) {
// MyClass类声明中的E形参是String类型。
// 泛型构造器中声明的T形参是Integer类型
MyClass<String> mc1 = new MyClass<>(5);

// 显式指定泛型构造器中声明的T形参是Integer类型,
MyClass<String> mc2 = new <Integer>MyClass<String>(5);

// MyClass类声明中的E形参是String类型。
// 如果显式指定泛型构造器中声明的T形参是Integer类型
// 此时就不能使用"菱形"语法,下面代码是错的。
MyClass<String> mc3 = new <Integer> MyClass<>(5);//1号代码
}
}

上面程序中1号代码既指定了泛型构造器中的泛型形参是 Integer类型,又想使用“菱形”语法,所以这行代码无法通过编译。

9.4.2 泛型方法和类型通配符的区别

大多数时候都可以使用泛型方法来代替类型通配符。例如,对于JavaCollection接口中两个方法定义:

1
2
3
4
5
6
7
...
public interface Collection<E> extends Iterable<E> {
...
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
...
}

上面集合中两个方法的形参都采用了类型通配符的形式,也可以采用泛型方法的形式,如下所示。

1
2
3
4
5
6
7
...
public interface Collection<E> extends Iterable<E> {
...
<T> boolean containsAll(Collection<T> c);
<T extends E> boolean addAll(Collection<T> c);
...
}

上面方法使用了<T extends E>泛型形式,这时定义泛型形参时设定上限(其中ECollection接口里定义的泛型,在该接口里E可当成普通类型使用)。

上面两个方法中泛型形参T只使用了一次,泛型形参T产生的唯一效果是可以在不同的调用点传入不同的实际类型。对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的。

泛型方法允许泛型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。

如果某个方法中一个形参(a)的类型或返回值的类型依赖于另一个形参(b)的类型,则形参(b)的类型声眀不应该使用通配符——因为形参(a)或返回值的类型依赖于该形参(b)的类型,如果形参(b)的类型无法确定,程序就无法定义形参(a)的类型。在这种情况下,只能考虑使用在方法签名中声明泛型,也就是泛型方法。

如果有需要,也可以同时使用泛型方法和通配符,如JavaCollections.copy()方法。

1
2
3
4
5
6
7
8
...
public class Collections {
...
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
...
}
...
}

上面copy方法中的destsrc存在明显的依赖关系,从源List中复制出来的元素,必须可以“丢进”目标List中,所以List集合元素的类型只能是目标集合元素的类型的子类型或者它本身。但JDK定义src形参类型时使用的是类型通配符,而不是泛型方法。这是因为:该方法无须向src集合中添加元素,也无须修改src集合里的元素,所以可以使用类型通配符,无须使用泛型方法。

提示:简而言之,指定上限的类型通配符支持协变,因此**这种协变的集合可以安全地取出元素(协变只出不进)**,因此无须使用泛型方法。

当然,也可以将上面的方法签名改为使用泛型方法,不使用类型通配符,如下所示

1
2
3
4
5
6
7
8
...
public class Collections {
...
public static <T,S extends T> void copy(List<T> dest, List<S> src) {
...
}
...
}

这个方法签名可以代替前面的方法签名。但注意上面的泛型形参S,它仅使用了一次,其他参数的类型、方法返回值的类型都不依赖于它,那泛型形参S就没有存在的必要,即可以用通配符来代替S。使用通配符比使用泛型方法(在方法签名中显式声明泛型形参)更加清晰和准确,因此Java设计该方法时采用了通配符,而不是泛型方法。

类型通配符与泛型方法(在方法签名中显式声明泛型形参)还有一个显著的区别:类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的泛型形参必须在对应方法中显式声明。

9.4 泛型方法

前面介绍了在定义类、接口时可以使用泛型形参,在该类的方法定义和成员变量定义、接口的方法定义中,这些泛型形参可被当成普通类型来用。在另外一些情况下,定义类、接口时没有使用泛型形参,但定义方法时想自己定义泛型形参,这也是可以的,Java 5还提供了对泛型方法的支持。

9.4.1 定义泛型方法

假设需要实现这样一个方法—该方法负责将一个Object数组的所有元素添加到一个Collection集合中。考虑采用如下代码来实现该方法。

1
2
3
4
5
6
7
static void fromArrayToCollection(Object[] a, Collection<object> c){
// 遍历a数组
for (object o: a){
// 把a数组中的每一个元素添加到c集合中
c.add(o);
}
}

上面定义的方法没有任何问题,关键在于方法中的c形参,它的数据类型是Collection<Object>正如前面所介绍的,Collections<String>不是Collection<Object>的子类型,所以这个方法的功能非常有限,它只能将Object数组的元素复制到元素为ObjectCollection集合中,无法复制到其他类型的Collection集合,即下面代码将引起编译错误。

1
2
3
4
5
6
// 定义String类型的数组
String[] strArr = {"a","b"};
// 创建泛型实参为String的List
List<String> strList= new ArrayList<>();
// Collection<String>对象不能当成Collection<String>使用,下面代码出现编译错误
fromArrayToCollection(strArr, strList);

可见上面fromArrayToCollection方法的参数类型不可以使用Collection<String>

那使用通配符Collection<?>是否可行呢?显然也不行,因为Java不允许把对象放进一个未知类型的集合中

为了解决这个问题,可以使用Java 5提供的泛型方法(Generic Method)。所谓泛型方法,就是在声明方法时定义一个或多个泛型形参。

泛型方法的语法格式

1
2
3
修饰符 <T,S> 返回值类型 方法名(形参列表){
//方法体
}

把上面方法的格式和普通方法的格式进行对比,不难发现泛型方法的方法签名比普通方法的方法签名多了泛型形参声明,

  • 泛型形参声明以英文尖括号<>括起来,
  • 多个泛型形参之间以硬逗号, 隔开,
  • 所有的泛型形参声明放在方法修饰符方法返回值类型之间

采用支持泛型的方法,就可以将上面的fromArrayToCollection方法改为如下形式:

1
2
3
4
5
6
7
8
// 修饰符 泛型形参声明 返回值 方法名 方法参数列表
static <T> void fromArrayToCollection (T[] a, Collection<T> c){
// 遍历a数组
for (T o:a){
// 添加a数组中的元素到集合c中
c.add(o);
}
}

下面程序示范了完整的用法。

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.util.*;

public class GenericMethodTest {
// 声明一个泛型方法,该泛型方法中带一个T泛型形参,
// 修饰符static,泛型形参T,返回值void,方法名fromArrayToCollection
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o);
}
}

public static void main(String[] args) {
// 定义一个Object类型的数组
Object[] objectArray = new Object[100];
// 定义一个保存Object元素的Collection
Collection<Object> objectCollection = new ArrayList<>();

// 实际参数1和实际参数2存放的都是Object的情况
// 下面代码中T代表Object类型
fromArrayToCollection(objectArray, objectCollection);

// 定义一个String类型的数组
String[] stringArray = new String[100];
// 定义一个String类型的Collection
Collection<String> stringCollection = new ArrayList<>();

// 实际参数1和实际参数2存放的都是String的情况
// 下面代码中T代表String类型
fromArrayToCollection(stringArray, stringCollection);

// 实际参数1存放的类型是String,实参2存放的是Object的情况。
// 下面代码中T代表Object类型
fromArrayToCollection(stringArray, objectCollection);

// 定义Integer的的数组
Integer[] integerArray = new Integer[100];
// 定义Float数组
Float[] floatArray = new Float[100];
// 定义Number数组
Number[] numberArray = new Number[100];
// 定义Number的Collection
Collection<Number> numberCollection = new ArrayList<>();

// 实参1存放的是Integer,实参2存放的是Number时
// 下面代码中T代表Number类型
fromArrayToCollection(integerArray, numberCollection);

// 实参1存放的是Float,实参2存放的是Number时
// 下面代码中T代表Number类型
fromArrayToCollection(floatArray, numberCollection);

// 实参1存放的是Number,实参2存放的时Number时
// 下面代码中T代表Number类型
fromArrayToCollection(numberArray, numberCollection);

// 实参1存放的是Number,实参2存放的是Object时
// 下面代码中T代表Object类型
fromArrayToCollection(numberArray, objectCollection);

// 下面代码中T代表String类型,但numberArray是一个Number数组,
// 因为Number既不是String类型,
// 也不是它的子类,所以出现编译错误
fromArrayToCollection(numberArray, stringCollection);
}
}

上面程序定义了一个泛型方法,该泛型方法中定义了一个T泛型形参,这个T类型就可以在该方法内当成普通类型使用。与接口、类声明中定义的泛型不同的是,方法声明中定义的泛型只能在该方法里使用,而接口、类声明中定义的泛型则可以在整个接口、类中使用。

与类、接口中使用泛型参数不同的是,方法中的泛型参数无须显式传入实际类型参数,如上面程序所示,当程序调用 fromArrayToCollection方法时,无须在调用该方法前传入 StringObject等类型,但系统依然可以知道为泛型实际传入的类型,因为编译器根据实参推断出泛型所代表的类型,它通常推断出最直接的类型。例如,下面调用代码

1
fromArrayToCollection(stringArray, stringCollection);

上面代码中stringCollection是一个 Collection<String>类型,与方法定义时的fromArrayToCollection(T[] a, Collection<T> c)进行比较——只比较泛型参数,不难发现该T类型代表的实际类型是 String类型。

对于如下调用代码:

1
fromArrayToCollection(integerArray, numberCollection);

上面的numberCollectionCollection<Number>类型,与此方法的方法签名进行比较—只比较泛型参数,不难发现该T类型代表了 Number类型。

为了让编译器能准确地推断出泛型方法中泛型的类型,不要制造迷惑!系统一旦迷惑了,就是你错了!看如下程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

import java.util.*;

public class ErrorTest {
// 声明一个泛型方法,该泛型方法中带一个T泛型形参
static <T> void test(Collection<T> from, Collection<T> to) {
for (T ele : from) {
to.add(ele);
}
}

public static void main(String[] args) {
List<Object> as = new ArrayList<>();
List<String> ao = new ArrayList<>();
// 下面代码将产生编译错误
// test(as, ao);
}
}

上面程序中定义了test()方法,该方法用于将前一个集合里的元素复制到下一个集合中,该方法中的两个形参fromto的类型都是 Collections<T>,这要求调用该方法时的两个集合实参中的泛型类型相同,否则编译器无法准确地推断出泛型方法中泛型形参的类型。

上面程序中调用test方法传入了两个实际参数,其中as的数据类型是 List<String>,而ao的数据类型是List<Object>,与泛型方法签名进行对比:test(Collection<T> a, Collection<T> c),编译器无法正确识别T所代表的实际类型。为了避免这种错误,可以将该方法改为如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.*;

public class RightTest {
// 声明一个泛型方法,该泛型方法中带一个T形参
static <T> void test(Collection<? extends T> from, Collection<T> to) {
for (T ele : from) {
to.add(ele);
}
}

public static void main(String[] args) {
List<Object> ao = new ArrayList<>();
List<String> as = new ArrayList<>();
// 下面代码完全正常
test(as, ao);
}
}

上面代码改变了test()方法签名,将该方法的前一个形参类型改为 Collection<? extends T>,这种采用类型通配符的表示方式,只要 test()方法的前一个 Collection集合里的元素类型是后一个 Collection集合里元素类型的子类即可。

那么这里产生了一个问题:到底何时使用泛型方法?何时使用类型通配符呢?接下来详细介绍泛型方法和类型通配符的区别。

9.3.4 设定泛型形参的上限

Java泛型不仅允许在使用通配符形参时设定上限,而且可以在定义泛型形参时设定上限,用于表示传给该泛型形参的实际类型要么是该上限类型,要么是该上限类型的子类

设置单个类上限

下面程序示范了这种用法。

1
2
3
4
5
6
7
8
9
10
11
public class Apple<T extends Number> {
T col;

public static void main(String[] args) {
Apple<Integer> ai = new Apple<>();
Apple<Double> ad = new Apple<>();
// 下面代码将引起编译异常,下面代码试图把String类型传给T形参
// 但String不是Number的子类型,所以引发编译错误
// Apple<String> as = new Apple<>(); // 代码1
}
}

上面程序定义了一个Apple泛型类,该Apple类的泛型形参的上限是Number类,这表明使用Apple类时为T形参传入的实际类型参数只能是NumberNumber类的子类。上面程序在代码1 处将引起编译错误:类型T的上限是Number类型,而此处传入的实际类型是String类型,既不是Number类型,也不是Number类型的子类型,所以将会导致编译错误。

可以设置一个类上限 多个接口上限

在一种更极端的情况下,程序需要为泛型形参设定多个上限(至多有一个父类上限,可以有多个接口上限),表明该泛型形参必须是其父类的子类(是父类本身也行),并且实现多个上限接口。如下代码所示。

1
2
3
4
5
import java.io.Serializable;
// 表明T类型必须是 Number类或其子类,并必须实现java.io.Serializable接口
public class Apple2<T extends Number & Serializable> {
// ......
}

类上限要写在接口上限前面

与类同时继承父类、实现接口类似的是,为泛型形参指定多个上限时,所有的接口上限必须位于类上限之后。也就是说,如果需要为泛型形参指定类上限,类上限必须位于第一位。

9.3.3 设定类型通配符的下限

除可以指定通配符的上限之外,Java也允许指定通配符的下限,通配符的下限用<? super 类型>的方式来指定,通配符下限的作用与通配符上限的作用恰好相反。

逆变

指定通配符的下限就是为了支持类型型变。比如SonFather的子类,当程序需要一个A<? super Father>变量时,程序可以将A<Father>A<Object>赋值给A<? super Father>类型的变量,这种型变方式被称为逆变

对于逆变的泛型集合来说,编译器只知道集合元素是下限的父类型,但具体是哪种父类型则不确定。因此,这种逆变的泛型集合能向其中添加元素(因为实际赋值的集合元素总是逆变声明的父类),从集合中取元素时只能被当成Object类型处理(编译器无法确定取出的到底是哪个父类的对象)。

假设自己实现一个工具方法:实现src集合中的元素复制到dest集合的功能,因为dest集合可以保存src集合中的所有元素,所以dest集合元素的类型应该是sre集合元素类型的父类

对于上面的copy方法,可以这样理解两个集合参数之间的依赖关系:不管src集合元素的类型是什么,只要dest集合元素的类型与前者相同或者是前者的父类即可,此时通配符的下限就有了用武之地。下面程序采用通配符下限的方式来实现该copy方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.*;

public class MyUtils {
// 下面dest集合元素类型必须与src集合元素类型相同,或是其父类
public static <T> T copy(List<? super T> dest, List<T> src) {
T last = null;
for (T ele : src) {
last = ele;
// 逆变的泛型集合添加元素是安全的
dest.add(ele);
}
return last;
}

public static void main(String[] args) {
List<Number> listNumber = new ArrayList<>();
List<Integer> listInteger = new ArrayList<>();
listInteger.add(5);
// 此处可准确的知道最后一个被复制的元素是Integer类型
// 与src集合元素的类型相同
Integer last = copy(listNumber, listInteger); // 代码1
System.out.println(listNumber);
}
}

使用这种语句,就可以保证程序的代码1 处调用后推断出最后一个被复制的元素类型是Integer,而不是笼统的Number类型。

上面方法用到了泛型方法的语法,就是在方法修饰符和返回值类型之间用<>定义泛型形参。关于泛型方法更详细介绍可参考下一节。

实际上,Java集合框架中的TreeSet<E>有一个构造器也用到了这种设定通配符下限的语法,如下所示:

1
2
3
4
5
6
7
8
9
package java.util;
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable {
...
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
...
}

正如前一章所介绍的, TreeSet会对集合中的元素按自然顺序定制顺序进行排序。如果需要TreeSet对集合中的所有元素进行定制排序,则要求TreeSet对象有一个与之关联的Comparator对象。上面构造器中的参数comparator就是进行定制排序的Comparator对象。

Comparator接口也是一个带泛型声明的接口

1
2
3
4
5
6
...
@FunctionalInterfacepublic
interface Comparator<T> {
int compare(T o1, T o2);
...
}

通过这种带下限的通配符的语法,可以在创建TreeSet对象时灵活地选择合适的Comparator。假定需要创建一个TreeSet<String>集合,并传入一个可以比较String大小的Comparator,这个Comparator既可以是Comparator<String>,也可以是Comparator<Object>——只要尖括号里传入的类型是String的父类型(或它本身)即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.*;

public class TreeSetTest {
public static void main(String[] args) {
// Comparator的实际类型是TreeSet的元素类型的父类,满足要求
TreeSet<String> ts1 = new TreeSet<>(new Comparator<Object>() {
public int compare(Object fst, Object snd) {
return hashCode() > snd.hashCode() ? 1 : hashCode() < snd.hashCode() ? -1 : 0;
}
});
ts1.add("hello");
ts1.add("wa");
// Comparator的实际类型是TreeSet元素的类型,满足要求
TreeSet<String> ts2 = new TreeSet<>(new Comparator<String>() {
public int compare(String first, String second) {
return first.length() > second.length() ? -1 : first.length() < second.length() ? 1 : 0;
}
});
ts2.add("hello");
ts2.add("wa");
System.out.println(ts1);
System.out.println(ts2);
}
}

通过使用这种通配符下限的方式来定义TreeSet构造器的参数,就可以将所有可用的Comparator作为参数传入,从而增加了程序的灵活性。当然,不仅TreeSet有这种用法, TreeMap也有类似的用法,具体的请查阅JavaAPI文档。

9.3.2 设定类型通配符的上限

当直接使用List<?>这种形式时,即表明这个List集合可以是任何泛型List的父类。但还有一种特殊的情形,程序不希望这个List<?>是任何泛型List的父类,只希望它代表某一类泛型List的父类。考虑一个简单的绘图程序,下面先定义三个形状类

1
2
3
4
5
// 定义一个抽象类Shape
public abstract class Shape
{
public abstract void draw(Canvas c);
}
1
2
3
4
5
6
7
8
9
// 定义Shape的子类Circle
public class Circle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("在画布" + c + "上画一个圆");
}
}
1
2
3
4
5
6
7
8
9
// 定义Shape的子类Rectangle
public class Rectangle extends Shape
{
// 实现画图方法,以打印字符串来模拟画图方法实现
public void draw(Canvas c)
{
System.out.println("把一个矩形画在画布" + c + "上");
}
}

上面定义了三个形状类,其中Shape是一个抽象父类,该抽象父类有两个子类:CircleRectangle。接下来定义一个Canvas类,该画布类可以画数量不等的形状(Shape子类的对象),那应该如何定义这个Canvas类呢?考虑如下的Canvas实现类。

1
2
3
4
5
6
7
8
9
10
import java.util.*;

public class Canvas {
// 同时在画布上绘制多个形状
public void drawAll(List<Shape> shapes) {
for (Shape s : shapes) {
s.draw(this);
}
}
}

注意上面的drawAll()方法的形参类型是List<Shape>,而List<Circle>并不是List<Shape>的子类型,因此,下面代码将引起编译错误。

1
2
3
4
5
List<Circle> circleList = new ArrayList<Circle>();
Canvas c = new Canvas();
// 由于List<Circle>并不是List<Shape>的子类型,
// 所以下面代码引发编译错误
c.drawAll(circleList);
1
Erasure of method drawAll(List<Shape>) is the same as another method in type Canvas

关键在于List<Circle>并不是List<Shape>的子类型,所以不能把List<Circle>对象当成List<Shape>使用。为了表示List<Circle>的父类,可以考虑使用List<?>,但此时从List<?>集合中取出的元素只能被编译器当成Object处理。为了表示List集合的所有元素是Shape的子类,Java泛型提供了被限制的泛型通配符。被限制的泛型通配符表示如下

1
2
//它表示泛型形参必须是Shape子类的List
List<? extends Shape>

有了这种被限制的泛型通配符,就可以把上面的Canvas程序中的drawAll方法:

// 同时在画布上绘制多个形状
public void drawAll(List<Shape> shapes) {
    for (Shape s : shapes) {
        s.draw(this);
    }
}

改为如下形式:

// 同时在画布上绘制多个形状,使用被限制的泛型通配符
public void drawAll(List<? extends Shape> shapes) {
    for (Shape s : shapes) {
        s.draw(this);
    }
}

Canvas改为如上形式,就可以把List<Circle>对象当成List<? extends Shape>使用。List<? extends Shape>可以表示List<Circle>List<Rectangle>的父类——只要List后尖括号里的类型是Shape的子类型即可。

指定通配符上限的集合只能取出元素

List<? extends Shape>受限制通配符的例子,此处的问号(?)代表一个未知的类型,就像前面看到的通配符一样。但是此处的这个未知类型一定是Shape的子类型(也可以是Shape本身),因此可以把Shape称为这个通配符的上限(upper bound)。

类似地,由于程序无法确定这个受限制的通配符的具体类型,所以不能把Shape对象或其子类的对象加入这个泛型集合中。例如,下面代码就是错误的。

1
2
3
4
public void addRectangle(List<? extends Shape> shapes){
//下面代码引起编译错误
shapes.add(0, new Rectangle());
}

与使用普通通配符相似的是, shapes.add()的第二个参数类型是? extends Shape,它表示Shape未知的子类,程序无法确定这个参数的类型具体是什么,所以无法将任何对象添加到这种集合中。

简而言之,这种指定通配符上限的集合,只能从集合中取元素(取出的元素总是上限的类型),不能向集合中添加元素(因为编译器没法确定集合元素实际是哪种子类型)。

对于更广泛的泛型类来说,指定通配符上限就是为了支持类型型变

协变

比如SonFather的子类,这样A<Father>就相当于A<? extends Son>的子类,可以将A<Father>赋值给A<? extends Son>类型的变量,这种型变方式被称为协变

https://blog.csdn.net/u010900754/article/details/101113667
什么是协变?协变其实指的是,如果基础类型具备父子关系,那么对应的容器类型也具备。
泛型不允许协变,而数组允许协变
数组的报错是在存元素时抛出的,而泛型的报错是在取元素是抛出的,这样,泛型的报错时机就非常延后了,如果类型不对,压根就不应该让这个元素放入,否则,就只能在读取时进行强转才能发现,可别小看这个时机问题,一旦发生,非常难定位,很难查到是在哪里放入了类型异常的元素,所以泛型不允许协变。原因就是,类型转换的问题需要延后到读取时才能发现。而数组则可以在存入时就检测到类型不匹配的问题,从而fail-fast。

https://blog.csdn.net/qq_37779333/article/details/113897799
在面向对象程序设计中,协变返回类型指的是子类中的成员函数的返回值类型不必严格等同于父类中被重写的成员函数的返回值类型,而可以是更”狭窄”的类型
Java 5.0添加了对协变返回类型的支持,即子类覆盖(即重写)基类方法时,返回的类型可以是基类方法返回类型的子类。协变返回类型允许返回更为具体的类型

https://www.cnblogs.com/stevenshen123/p/9215750.html

  • ? extends 对应 协变,
  • ? super 对应 逆变。

协变只出不进

对于协变的泛型类来说,它只能调用泛型类型作为返回值类型的方法(编译器会将该方法返回值当成通配符上限的类型);而不能调用泛型类型作为参数的方法。口诀是:协变只出不进!
对于指定通配符上限的泛型类,相当于通配符上限是Object

9.3.1 使用类型通配符

为了表示各种泛型List的父类,可以使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:List<?>(意思是元素类型未知的List)。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型。可以将上面方法改写为如下形式:

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

现在使用任何类型的List来调用它,程序依然可以访问集合c中的元素,其类型是Object,这永远是安全的,因为不管List的真实类型是什么,它包含的都是Object

通配符?可以作为任意泛型形参的实参

上面程序中使用的List<?>,其实这种写法可以适应于任何支持泛型声明的接口和类,比如写成Set<?>Collection<?>Map<?,?>
但这种带通配符的List<?>仅表示它是各种泛型List的父类,并不能把元素加入到其中。例如,如下代码将会引起编译错误:

1
2
3
List<?> list= new ArrayList<String>();
//下面程序引起编译错误
list.add(new Object());

因为程序无法确定c集合中元素的类型,所以不能向其中添加对象。根据前面的List<E>接口定义的代码可以发现:add()方法有类型参数E作为集合的元素类型,所以传给add的参数必须是E类的对象或者其子类的对象。但因为在该例中不知道E是什么类型,所以程序无法将任何对象“丢进”该集合。唯一的例外是null因为null是所有引用类型的实例

另一方面,程序可以调用get()方法来返回List<?>集合指定索引处的元素,其返回值是一个未知类型,但可以肯定的是,它总是一个Object。因此,把get()的返回值赋值给一个Object类型的变量,或者放在任何希望是Object类型的地方都可以。

9.3 类型通配符

正如前面讲的,当使用一个泛型类时(包括声明变量和创建对象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数,编译器就会提出泛型警告。假设现在需要定义一个方法,该方法里有一个集合形参,集合形参的元素类型是不确定的,那应该怎样定义呢?

考虑如下代码:

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

上面程序当然没有问题:这是一段最普通的遍历List集合的代码。问题是上面程序中List是一个有泛型声明的接口,此处使用Lit接口时没有传入实际类型参数,这将引起泛型警告。为此,考虑为List接口传入实际的类型参数—因为List集合里的元素类型是不确定的,将上面方法改为如下形式。

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

表面上看起来,上面方法声明没有问题,这个方法声明确实没有任何问题。问题是调用该方法传入的实际参数值时可能不是我们所期望的,例如,下面代码试图调用该方法。

1
2
3
4
//创建一个List<String>对象
List<String> strList=new ArrayList<>()
//将strList作为参数来调用前面的test方法
test(strList);//①

编译上面程序,将在①处发生如下编译错误。

1
无法将Test中的test(java.util.List<java.lang.Object>)应用于(java.util.List<java.lang.String>)

List<String>类并不是List<Object>类的子类

上面程序出现了编译错误,这表明List<String>对象不能被当成List<Object>对象使用,也就是说,List<String>类并不是List<Object>类的子类。

如果SonFather的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口G<Son>并不是G<Father>的子类型!这一点非常值得注意,因为它与大部分人的习惯认为是不同

与数组进行对比,先看一下数组是如何工作的。在数组中,程序可以直接把一个Integer[]数组赋给个Number[]变量。如果试图把一个Double对象保存到该Number[]数组中,编译可以通过,但在运行时抛出ArrayStoreException异常。例如如下程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.*;

public class ArrayErr {
public static void main(String[] args) {
// 定义一个Integer数组
Integer[] ia = new Integer[5];
// 可以把一个Integer[]数组赋给Number[]变量
Number[] na = ia;
// 下面代码编译正常,但运行时会引发ArrayStoreException异常
// 因为0.5并不是Integer
na[0] = 0.5; // ①
}
}

上面程序在①号粗体字代码处会引发ArrayStoreException运行时异常,这就是一种潜在的风险

Java的早期设计中,允许Integer[]数组赋值给 Number[]变量存在缺陷,因此Java在泛型设计时进行了改进,它不再允许把List<Integer>对象赋值给List<Number>变量。例如,如下代码将会导致编译错误(程序清单同上)。

1
2
3
List<Integer> iList = new ArrayList<>();
// 下面代码导致编译错误
List<Number> nList = iList;

Java泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常.

Java的数组支持型变,但Java集合并不支持型变

数组和泛型有所不同,假设SonFather的一个子类型(子类或者子接口),那么Son[]依然是Father[]的子类型;但G<Son>不是G<Father>的子类型。

型变

Son[]自动向上转型为Father[]的方式被称为型变。也就是说,Java的数组支持型变,但Java集合并不支持型变