9.3 类型通配符
9.3 类型通配符
正如前面讲的,当使用一个泛型类时(包括声明变量和创建对象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数,编译器就会提出泛型警告。假设现在需要定义一个方法,该方法里有一个集合形参,集合形参的元素类型是不确定的,那应该怎样定义呢?
考虑如下代码:
1 | public void test(List c){ |
上面程序当然没有问题:这是一段最普通的遍历List
集合的代码。问题是上面程序中List
是一个有泛型声明的接口,此处使用Lit
接口时没有传入实际类型参数,这将引起泛型警告。为此,考虑为List
接口传入实际的类型参数—因为List
集合里的元素类型是不确定的,将上面方法改为如下形式。
1 | public void test(List<Object> c){ |
表面上看起来,上面方法声明没有问题,这个方法声明确实没有任何问题。问题是调用该方法传入的实际参数值时可能不是我们所期望的,例如,下面代码试图调用该方法。
1 | //创建一个List<String>对象 |
编译上面程序,将在①处发生如下编译错误。
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>
类的子类。
如果Son
是Father
的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口G<Son>
并不是G<Father>
的子类型!这一点非常值得注意,因为它与大部分人的习惯认为是不同
与数组进行对比,先看一下数组是如何工作的。在数组中,程序可以直接把一个Integer[]
数组赋给个Number[]
变量。如果试图把一个Double
对象保存到该Number[]
数组中,编译可以通过,但在运行时抛出ArrayStoreException
异常。例如如下程序。
1 | import java.util.*; |
上面程序在①号粗体字代码处会引发ArrayStoreException
运行时异常,这就是一种潜在的风险
在Java
的早期设计中,允许Integer[]
数组赋值给 Number[]
变量存在缺陷,因此Java
在泛型设计时进行了改进,它不再允许把List<Integer>
对象赋值给List<Number>
变量。例如,如下代码将会导致编译错误(程序清单同上)。
1 | List<Integer> iList = new ArrayList<>(); |
Java
泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException
异常.
Java
的数组支持型变,但Java
集合并不支持型变
数组和泛型有所不同,假设Son
是Father
的一个子类型(子类或者子接口),那么Son[]
依然是Father[]
的子类型;但G<Son>
不是G<Father>
的子类型。
型变
Son[]
自动向上转型为Father[]
的方式被称为型变。也就是说,Java
的数组支持型变,但Java
集合并不支持型变