文档

Java™ 教程-Java Tutorials 中文版
通配符
Trail: Bonus
Lesson: Generics

通配符

考虑编写打印出集合中所有元素的例程的问题。以下是你可以使用旧版本的语言(即 5.0 之前的版本)编写它的方法:

void printCollection(Collection c) {
    Iterator i = c.iterator();
    for (k = 0; k < c.size(); k++) {
        System.out.println(i.next());
    }
}

这是一个简单的尝试使用泛型(和新的 for 循环语法)编写它:

void printCollection(Collection<Object> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

问题是这个新版本比旧版本的用处小得多。虽然可以使用任何类型的集合作为参数调用旧代码,但新代码仅可以采用 Collection<Object>,正如我们刚才演示的那样,它 不是 各种集合的超类型!

那么什么 各类集合的超类型?它写成 Collection<?>(读作“collection of unknown”),即元素类型与任何东西匹配的集合。出于显而易见的原因,它被称为 wildcard type (通配符类型)。我们可以写:

void printCollection(Collection<?> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

现在,我们可以用任何类型的集合来调用它。请注意,在 printCollection() 中,我们仍然可以从 c 中读取元素,并为它们提供类型 Object。这总是安全的,因为无论集合的实际类型如何,它都包含对象。然而,向它添加任意对象是不安全的:

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // Compile time error

由于我们不知道 c 的元素类型代表什么,我们无法向其添加对象。add() 方法接受类型 E 的参数,即集合的元素类型。当实际类型形参是 ? 时,它代表某种未知类型。我们传递给 add 的任何参数都必须是这种未知类型的子类型。既然我们不知道那是什么类型,我们就无法传递任何东西。唯一的例外是 null,它是每种类型的成员。

另一方面,给定 List<?>,我们 可以 调用 get() 并使用结果。结果类型是未知类型,但我们始终知道它是一个对象。因此,将 get() 的结果赋值给 Object 类型的变量,或者将其传递以作为预期 Object 类型的参数是安全的。

有界通配符

考虑一个简单的绘图应用程序,可以绘制矩形和圆形等形状。要在程序中表示这些形状,你可以定义类层次结构,例如:

public abstract class Shape {
    public abstract void draw(Canvas c);
}

public class Circle extends Shape {
    private int x, y, radius;
    public void draw(Canvas c) {
        ...
    }
}

public class Rectangle extends Shape {
    private int x, y, width, height;
    public void draw(Canvas c) {
        ...
    }
}

这些类可以在画布上绘制:

public class Canvas {
    public void draw(Shape s) {
        s.draw(this);
   }
}

任何绘图通常都包含许多形状。假设它们被表示为一个列表,那么在 Canvas 中创建一个方法会很方便:

public void drawAll(List<Shape> shapes) {
    for (Shape s: shapes) {
        s.draw(this);
   }
}

现在,类型规则说 drawAll() 只能在 Shape 的列表上调用:例如,它不能在 List<Circle> 上调用。这是不幸的,因为所有方法都是从列表中读取形状,所以它也应该可以在 List<Circle> 上调用。我们真正想要的是接受 任何 形状列表的方法:

public void drawAll(List<? extends Shape> shapes) {
    ...
}

这里有一个很小但非常重要的区别:我们用 List<? extends Shape> 替换了 List<Shape>。现在 drawAll() 将接受 Shape 的任何子类的列表,因此我们现在可以根据需要在 List<Circle> 上调用它。

List<? extends Shape>bounded wildcard (有界通配符) 的示例。? 代表一种未知类型,就像我们之前看到的通配符一样。但是,在这种情况下,我们知道这种未知类型实际上是 Shape 的子类型。(注意:它可能是 Shape 本身,或者某些子类;它不需要字面上继承 Shape。)我们说 Shape 是通配符的 upper bound (上界)

像往常一样,为使用通配符的灵活性付出了代价。这个代价是在方法体中写入 shapes 现在是非法的。例如,这是不允许的:

public void addRectangle(List<? extends Shape> shapes) {
    // Compile-time error!
    shapes.add(0, new Rectangle());
}

你应该能够弄清楚为什么不允许上面的代码。shapes.add() 的第二个参数的类型是 ? extends Shape - Shape 的未知子类型。由于我们不知道它是什么类型,我们不知道它是否是 Rectangle 的超类型;它可能是也可能不是这样的超类型,因此在那里传递 Rectangle 是不安全的。

有界通配符正是人们需要处理 DMV 将其数据传递给人口普查局的例子。我们的示例假定数据通过从名称(表示为字符串)到人(由引用类型表示,例如 Person 或其子类型,例如 Driver)的映射来表示。Map<K,V> 是一个泛型类型的示例,它采用两个类型实参,表示 map 的键和值。

再次,请注意形式类型形参的命名规范 - K 表示 keys 且 V 表示 values。

public class Census {
    public static void addRegistry(Map<String, ? extends Person> registry) {
}
...

Map<String, Driver> allDrivers = ... ;
Census.addRegistry(allDrivers);

Previous page: Generics and Subtyping
Next page: Generic Methods