9.3 类型通配符

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集合并不支持型变