Java 学习之路 之 类型通配符(四十一)

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

考虑如下代码:

public void test(List c){  for (int i = 0; i < c.size(); i++){    System.out.println(c.get(i));

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

public void test(List<Object> c){  for(int i = 0; i < c.size(); i++){    System.out.println(c.get(i));

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

List<String> strList = new ArrayList<>();// 将 strList 作为参数来调用前面的 test 方法

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

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

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

如果 Foo 是 Bar 的一个子类型(子类或者子接口),而 G 是具有泛型声明的类或接口,G<Foo> 并不是 G<Bar> 的子类型!这一点非常值得注意,因为它与我们的习惯看法不同。

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

public class ArrayError {public static void main(String[] args) {Integer[] ia = new Integer[5];// 可以把一个 Integer[] 数组赋给 Number[] 变量// 下面代码编译正常,但运行时会引发 ArrayStoreException 异常

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

一门设计优秀的语言,不仅需要提供强大的功能,而且能提供强大的“错误提示”和“出错警告”,这样才能尽量避免开发者犯错。而 Java 允许 Integer[] 数组赋值给 Number[] 变量显然不是一种安全的设计。

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

List<Integer> iList = new ArrayList<>();List<Number> nList = iList;

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

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

1,使用类型通配符

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

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 的父类,并不能把元素加入到其中。例如,如下代码将会引起编译错误。

List<?> c = new ArrayList<String>();

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

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

2,设定类型通配符的上限

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

public abstract class Shape{  public abstract void draw(Canvas c);
public class Circle extends Shap{  // 实现画图方法,以打印字符串来模拟画图方法实现  public void draw(Canvas c){    System.out.println("在画布" + c +"上画一个圆");
// 定义 Shape 的子类 Rectanglepublic class Rectangle extends shape{  public void draw(Canvas c){    System.out.println("把一个矩形画在画布" + c + "上");

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

  public void  drawAll(List<Shape> shapes){

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

List<Circle> circleList = new ArrayList<>();// 不能把 List<Circle> 当成 List<Shape>使用,所以下面代码引起编译错误

关键在于 List<Circle> 并不是 List<Shape> 的子类型,所以不能把 List<Circle> 对象当成 List<Shape> 使用。为了表示 List<Circle> 的父类,可以考虑使用 List<?>,把 Canvas 改为如下形式(程序清单同上):

  public void drawAll(List<?> shapes){    for(Object obj : shapes){

上面程序使用了通配符来表示所有的类型。上面的 drawAll() 方法可以接受 List<Circle> 对象作为参数,问题是上面的方法实现体显得极为臃肿而烦琐:使用了泛型还需要强制类型转换。

实际上,我们需要一种泛型表示方法,它可以表示所有 Shape 泛型 List 的父类。为了满足这种需求, Java 泛型提供了被限制的泛型通配符。被限制的泛型通配符表示如下:

//它表示所有 Shape 泛型 List 的父类

有了这种被限制的泛型通配符,我们就可以把上面的 Canvas 程序改为如下形式(程序清单同上):

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

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

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

因为我们不知道这个受限制的通配符的具体类型,所以不能把 Shape 对象或其子类的对象加入这个泛型集合中。例如下面代码就是错误的。

public void  addRectangle(List<? extends Shape> shapes){  shapes.add(0, new Rectangle());

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

3,设定类型形参的上限

Java 泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型形参时设定上限,用于表示传给该类型形参的实际类型要么是该上限类型,要门是该上限类型的子类。下面程序示范了这种用法。

public class Apple<T extends Number> {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 形参传入的实际类型参数只能是 Number 或 Number 类的子类。上面程序在 1 出将引起编译错误:类型形参 T 的上限是 Number 类型,而此处传入的实际类型是 String 类型,既不是 Number 类型,也不是 Number 类型的子类型,所以将会导致编译错误。

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

//表明 T 类型必须是 Number 类或其子类,并必须实现 java.io.Serializablepublic class Apple<T extends Number & java.io.Serilizable>{

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

首页 - Wiki
Copyright © 2011-2024 iteam. Current version is 2.125.3. UTC+08:00, 2024-05-21 18:45
浙ICP备14020137号-1 $访客地图$