这些Java教程是针对JDK 8编写的。本页面中描述的示例和实践不利用后续版本中引入的改进,并且可能使用不再可用的技术。
有关Java SE 9及其后续版本中更新的语言功能的摘要,请参阅Java语言更改。
有关所有JDK版本的新功能、增强功能和已删除或已弃用选项的信息,请参阅JDK发行说明。
考虑编写一个打印集合中所有元素的例程的问题。以下是如何在旧版本的语言(即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<?>
(发音为"unknown的集合"),也就是说,一个元素类型匹配任何类型的集合。它被称为通配符类型,原因是显而易见的。我们可以这样写:
void printCollection(Collection<?> c) { for (Object e : c) { System.out.println(e); } }
现在,我们可以使用任何类型的集合调用它。请注意,在printCollection()
内部,我们仍然可以从c
中读取元素,并将它们的类型视为Object
。这总是安全的,因为无论集合的实际类型是什么,它都包含对象。但是,向其中添加任意对象是不安全的:
Collection<?> c = new ArrayList<String>(); c.add(new Object()); // 编译时错误
因为我们不知道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>
是一个有界通配符的示例。?
代表一个未知类型,就像我们之前看到的通配符一样。然而,在这种情况下,我们知道这个未知类型实际上是Shape
的子类型。(注意:它可以是Shape
本身,或者是某个子类;它不一定要直接继承Shape
。)我们说Shape
是通配符的上界。
通常,使用通配符的灵活性是需要付出代价的。代价是在方法体中不允许向shapes
写入。例如,下面的代码是不允许的:
public void addRectangle(List<? extends Shape> shapes) { // 编译时错误! shapes.add(0, new Rectangle()); }
你应该能够理解为什么上面的代码是不允许的。对shapes.add()
的第二个参数的类型是? extends Shape
- Shape
的一个未知子类型。由于我们不知道它的类型,我们不知道它是否是Rectangle
的超类型;它可能是也可能不是这样的超类型,因此在这里传递一个Rectangle
是不安全的。
有界通配符是处理DMV将其数据传递给人口普查局的示例所需的东西。我们的示例假设数据由从名称(表示为字符串)到人员(表示为引用类型,如Person
或其子类型,如Driver
)的映射来表示。 Map<K,V>
是一个接受两个类型参数的泛型类型,表示映射的键和值。
再次注意正式类型参数的命名约定--键使用K
,值使用V
。
public class Census { public static void addRegistry(Map<String, ? extends Person> registry) { } ... Map<String, Driver> allDrivers = ... ; Census.addRegistry(allDrivers);