[译] 4.通配符

本文深入探讨了Java泛型中的通配符概念,包括无界和有界通配符的使用,以及如何利用通配符提高代码的通用性和安全性。通过具体示例,解释了通配符如何解决集合操作中的类型限制问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

<< 泛型和子类型 · 目录 · 泛型方法 >>

                                                                                  

通配符 (Wildcards) 

考虑一个问题:编写一个程序打印出集合中所有的元素。你可能会用旧版本的语言编写如下方法(比如JDK5.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<Object>不是任何集合的超类型(supertype) 。

那么,什么是所有集合的超类型呢?它写作 Collection<?> (读作 未知类型的集合),是元素类型匹配任何类型的集合。它被称为通配符类型,原因很明显。我们可以这样写:

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。

有界通配符 (Bounded Wildcards)

思考一个简单的绘图程序,可以绘制如矩形、圆等形状。用程序描述这些图形,你可以定义这样的类层次结构:

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);
   }
}

现在,按照类型规则,只有在 List<Shape> 上才能调用drall(),就比如,不能在 List<Circle> 上调用它。这很不幸,因为所有的方法都是从列表读取形状的,因此它也要能在 List<Circle> 上调用。我们真正想要的是该方法接受任何类型的列表:

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

这里有一个很小但很重要的区别:我们已经将类型 List 替换为 List<? extends Shape> 。现在,drallAll() 能接受任何 Shape 子类的列表,因此,如果需要,我们可以在 List<Circle> 上调用它。

List<? extends Shape> 是有界通配符的例子之一,那个?表示未知类型,就像我们之前看到的通配符一样。然而,在这个例子中,我们知道这种未知类型实际上是 Shape 的子类型(注意:它可以是 Shape 本身,也可以是某个子类;它不需要字面上扩展形状)。我们说 Shape 是通配符的上界。

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

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

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

有界通配符正是处理机动车辆部门将其数据传递给人口普查局的例子所需要的。我们的例子 假设数据由姓名(表示为字符串)映射到人(由引用类型如 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);

                                                                                  

<< 泛型和子类型 · 目录 · 泛型方法 >>

本文译自: https://docs.oracle.com/javase/tutorial/extra/generics/wildcards.html

转载于:https://my.oschina.net/tita/blog/2886585

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值