91

最近看了这篇文章:http: //download.oracle.com/javase/tutorial/extra/generics/wildcards.html

我的问题是,而不是创建这样的方法:

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

我可以创建这样的方法,并且效果很好:

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

我应该使用哪种方式?通配符在这种情况下有用吗?

4

5 回答 5

143

这取决于你需要做什么。如果你想做这样的事情,你需要使用有界类型参数:

public <T extends Shape> void addIfPretty(List<T> shapes, T shape) {
    if (shape.isPretty()) {
       shapes.add(shape);
    }
}

这里我们有 aList<T> shapes和 a T shape,因此我们可以安全地shapes.add(shape). 如果它被声明List<? extends Shape>,你不能安全地add使用它(因为你可能有 aList<Square>和 a Circle)。

因此,通过为有界类型参数命名,我们可以选择在泛型方法的其他地方使用它。当然,并不总是需要这些信息,因此如果您不需要了解太多关于类型的信息(例如您的drawAll),那么通配符就足够了。

即使您不再引用有界类型参数,如果您有多个边界,仍然需要有界类型参数。这是来自Angelika Langer 的 Java 泛型常见问题解答的引述

通配符绑定和类型参数绑定有什么区别?

通配符只能有一个界限,而类型参数可以有多个界限。通配符可以有下限或上限,而类型参数没有下限。

通配符界限和类型参数界限经常被混淆,因为它们都被称为界限,并且在某种程度上具有相似的语法。[…]

语法

  type parameter bound     T extends Class & Interface1 & … & InterfaceN

  wildcard bound  
      upper bound          ? extends SuperType
      lower bound          ? super   SubType

通配符只能有一个界限,即下限或上限。不允许使用通配符边界列表。

相反,一个类型参数可以有多个界限,但没有类型参数的下限之类的东西。

引自Effective Java 2nd Edition,Item 28:使用有界通配符来提高 API 灵活性

为了获得最大的灵活性,请在代表生产者或消费者的输入参数上使用通配符类型。[...] PECS 代表生产者extends-,消费者super- [...]

不要使用通配符类型作为返回类型。它不会为您的用户提供额外的灵活性,而是会迫使他们在客户端代码中使用通配符类型。如果使用得当,通配符类型对类的用户几乎是不可见的。它们使方法接受它们应该接受的参数并拒绝它们应该拒绝的参数。如果该类的用户必须考虑通配符类型,则该类的 API 可能有问题

应用 PECS 原则,我们现在可以回到我们的addIfPretty示例,并通过编写以下代码使其更加灵活:

public <T extends Shape> void addIfPretty(List<? super T> list, T shape) { … }

现在我们可以addIfPretty说,a Circle,到 a List<Object>。这显然是类型安全的,但我们最初的声明不够灵活,无法允许。

相关问题


概括

  • 请务必使用有界类型参数/通配符,它​​们会增加 API 的灵活性
  • 如果类型需要多个参数,你别无选择,只能使用有界类型参数
  • 如果类型需要下限,则别无选择,只能使用有界通配符
  • “生产者”有上限,“消费者”有下限
  • 不要在返回类型中使用通配符
于 2010-08-15T08:29:21.500 回答
6

在您的示例中,您实际上并不需要使用 T,因为您不会在其他任何地方使用该类型。

但如果你做了类似的事情:

public <T extends Shape> T drawFirstAndReturnIt(List<T> shapes){
    T s = shapes.get(0);
    s.draw(this);
    return s;
}

或者像 polygenlubricants 所说,如果你想将列表中的类型参数与另一个类型参数匹配:

public <T extends Shape> void mergeThenDraw(List<T> shapes1, List<T> shapes2) {
    List<T> mergedList = new ArrayList<T>();
    mergedList.addAll(shapes1);
    mergedList.addAll(shapes2);
    for (Shape s: mergedList) {
        s.draw(this);
    }
}

在第一个示例中,您将获得更多的类型安全性,然后只返回 Shape,因为您可以将结果传递给可能采用 Shape 子级的函数。例如,您可以将 a 传递List<Square>给我的方法,然后将生成的 Square 传递给只接受 Squares 的方法。如果您使用“?” 您必须将生成的 Shape 转换为 Square ,这不是类型安全的。

在第二个示例中,您确保两个列表具有相同的类型参数(您不能使用 '?' 来做到这一点,因为每个 '?' 都是不同的),以便您可以创建一个包含来自它们的所有元素的列表.

于 2010-08-15T08:50:54.193 回答
2

考虑以下来自 James Gosling 第 4 版的 Java 编程示例,我们希望在其中合并 2 个 SinglyLinkQueue:

public static <T1, T2 extends T1> void merge(SinglyLinkQueue<T1> d, SinglyLinkQueue<T2> s){
    // merge s element into d
}

public static <T> void merge(SinglyLinkQueue<T> d, SinglyLinkQueue<? extends T> s){
        // merge s element into d
}

上述两种方法具有相同的功能。那么哪个更可取?答案是第2个。用作者自己的话来说:

“一般规则是尽可能使用通配符,因为带有通配符的代码通常比带有多个类型参数的代码更具可读性。在决定是否需要类型变量时,问问自己该类型变量是否用于关联两个或多个参数,或将参数类型与返回类型相关联。如果答案是否定的,那么通配符就足够了。

注意:书中只给出了第二种方法,类型参数名称是 S 而不是 'T'。书中没有第一种方法。

于 2015-07-19T00:26:20.217 回答
1

据我了解,通配符允许在不需要类型参数的情况下使用更简洁的代码(例如,因为它在多个地方被引用,或者因为需要多个边界,如其他答案中详述)。

在您指出的链接中,我阅读了(在“通用方法”下)以下暗示这个方向的陈述:

泛型方法允许使用类型参数来表达方法和/或其返回类型的一个或多个参数的类型之间的依赖关系。如果不存在这样的依赖关系,则不应使用泛型方法。

[...]

使用通配符比声明显式类型参数更清晰、更简洁,因此应尽可能首选。

[...]

通配符还具有可以在方法签名之外使用的优点,如字段、局部变量和数组的类型。

于 2018-03-15T13:41:04.517 回答
0

第二种方式有点冗长,但它允许您在T其中引用:

for (T shape : shapes) {
    ...
}

据我所知,这是唯一的区别。

于 2010-08-15T08:28:18.290 回答