【问题标题】:Java: bounded wildcards or bounded type parameter?Java:有界通配符或有界类型参数?
【发布时间】:2010-08-15 08:19:09
【问题描述】:

最近,我读到了这篇文章: 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);
    }
}

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

【问题讨论】:

  • ?是一种速记符号;无论如何,内部编译器都会用类型参数替换它;当出现编译器错误时,您会看到代理类型参数,而不是 ?
  • 更正:我之前的评论是错误的。通配符比我想象的要复杂。
  • @irreputable 虽然确实更复杂,但您关于用类型参数替换它的观点是完全有效的;它只是一个你不能声明的。
  • 如果我们看just这两种方法 - 没有区别,只是风格问题;否则(如果你添加一些更通用的参数),事情会改变

标签: java api generics bounded-wildcard


【解决方案1】:

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

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

这里我们有一个List&lt;T&gt; shapes 和一个T shape,因此我们可以安全地shapes.add(shape)。如果它被声明为List&lt;? extends Shape&gt;,你可以安全地add 给它(因为你可能有一个List&lt;Square&gt; 和一个Circle)。

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

即使您不再引用有界类型参数,如果您有多个边界,仍然需要有界类型参数。这是Angelika Langer's Java Generics FAQs的引述

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

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

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

语法

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

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

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

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

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

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

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

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

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

现在我们可以将addIfPretty,比如Circle,转为List&lt;Object&gt;。这显然是类型安全的,但我们最初的声明不够灵活,无法允许它。

相关问题


总结

  • 请务必使用有界类型参数/通配符,它​​们可以提高 API 的灵活性
  • 如果类型需要多个参数,则只能使用有界类型参数
  • 如果类型需要下限,则别无选择,只能使用有界通配符
  • “生产者”有上限,“消费者”有下限
  • 不要在返回类型中使用通配符

【讨论】:

  • 非常感谢,你的解释很清楚很有启发性
  • @Tony:本书还简短讨论了在没有限制时如何在通配符和类型参数之间进行选择。本质上,如果类型参数在方法声明中只出现一次,请使用通配符。另请参阅 JLS 中的 reverse(List&lt;?&gt;) 示例 java.sun.com/docs/books/jls/third_edition/html/…
  • 我得到以下代码的 UnsupportedOperationException,public static <t extends number> void add(List super T&gt; list, T num) { list.add(num); }</t>
  • 另外 - 你不能像 doWork(? type) 这样将 ? 作为方法参数传递,因为那样你就不能在方法中使用该参数。您需要使用类型参数。
  • @VivekVardhan 您不能使用通配符创建这样的方法,这就是重点。在决定使用哪个时,this 是最好的解释来源之一。
【解决方案2】:

在您的示例中,您实际上并不需要使用 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 子级的函数。例如,您可以将List&lt;Square&gt; 传递给我的方法,然后将生成的 Square 传递给只接受 Squares 的方法。如果您使用“?”您必须将生成的 Shape 转换为 Square ,这不是类型安全的。

在第二个示例中,您确保两个列表具有相同的类型参数(不能使用 '?',因为每个 '?' 不同),以便您可以创建一个包含所有元素的列表他们两个。

【讨论】:

  • 我认为Shape s = ... 应该是T s = ...,否则return s; 不应该编译。
【解决方案3】:

考虑下面 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'。第一种方法书中没有。

【讨论】:

    【解决方案4】:

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

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

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

    [...]

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

    [...]

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

    【讨论】:

      【解决方案5】:

      第二种方式有点冗长,但它允许你在里面引用T

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

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

      【讨论】:

        猜你喜欢
        • 2015-08-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-10-29
        • 1970-01-01
        相关资源
        最近更新 更多