【问题标题】:Generics..? Super T [duplicate]泛型..?超级T [重复]
【发布时间】:2010-10-10 02:42:08
【问题描述】:

可能重复:
what is the difference between ‘super’ and ‘extends’ in Java Generics

一)

List<? super Shape> shapeSuper = new ArrayList<Shape>();

shapeSuper.add(new Square());       //extends from SHAP  
shapeSuper.add(new DoubleSquare()); //extends from SQ  
shapeSuper.add(new TripleSquare()); //extends from DS  
shapeSuper.add(new Rectangle());    //extends from SHAP  
shapeSuper.add(new Circle());       //extends from SHAP  

for (Object object : shapeSuper) { ... }

当我只能添加 Shape 及其对象时,为什么迭代必须是 Objects 衍生品?

B)

List<? super Shape> shapeSuper = new ArrayList<Object>();  

shapeSuper.add(new Object()); //compilation error  

为什么上面的行会产生编译错误?

【问题讨论】:

  • 出于好奇,这样做比定义一个 Shape 接口然后只创建一个 List 有什么好处?您仍然可以测试特定的类并在必要时进行强制转换...

标签: java generics


【解决方案1】:

对于您的示例,您可以像 Dan 和 Paul 所说的那样使用普通的 List&lt;Shape&gt;;您不需要使用通配符问号语法,例如List&lt;? super Shape&gt;List&lt;? extends Shape&gt;)。我认为您的潜在问题可能是,“我什么时候使用问号样式声明之一?” (Julien 引用的 Get 和 Put 原则是对这个问题的一个很好的回答,但我认为除非您在示例的上下文中看到它,否则它没有多大意义。)这是我对 Get 和说明何时使用通配符的原则。

如果...则使用&lt;? extends T&gt;

  • 一个方法有一个泛型类 参数Foo&lt;T&gt;readSource
  • 方法从 readSource 获取 T 的实例,并不关心检索到的实际对象是否属于 T 的子类。

如果...则使用&lt;? super T&gt;

  • 一个方法有一个泛型类参数Foo&lt;T&gt;writeDest
  • 该方法将 T 的实例放入 writeDest,并且不关心 writeDest 是否还包含属于 T 子类的对象。

下面是一个具体示例的演练,说明了通配符背后的思想。假设您正在编写一个 processSquare 方法,该方法从列表中删除一个正方形,对其进行处理,并将结果存储在一个输出列表中。这是一个方法签名:

void processSquare(List<Square> iSqua, List<Square> oSqua)
{ Square s = iSqua.remove(0); s.doSquare(); oSqua.add(s); }

现在您创建一个扩展 Square 的 DoubleSquares 列表,并尝试处理它们:

List<DoubleSquare> dsqares = ... 
List<Square> processed = new ArrayList<Square>;
processSquare(dsqares, processed); // compiler error! dsquares is not List<Square>

编译器因错误而失败,因为 dsquares List&lt;DoubleSquare&gt; 的类型与 processSquare 的第一个参数 List&lt;Square&gt; 的类型不匹配。也许 DoubleSquare is-a Square,但您需要告诉编译器 List&lt;DoubleSquare&gt; is-a List&lt;Square&gt; 以用于您的 processSquare 方法。 使用&lt;? extends Square&gt; 通配符告诉编译器您的方法可以采用Square 的任何子类的List。

void processSquare(List<? extends Square> iSqua, List<Square> oSqua)

接下来,您将改进应用程序以处理圆形和正方形。您希望将所有已处理的形状聚合到一个包含圆形和正方形的列表中,因此您将已处理列表的类型从 List&lt;Square&gt; 更改为 List&lt;Shape&gt;

List<DoubleSquare> dsqares = ... 
List<Circle> circles = ... 
List<Shape> processed = new ArrayList<Square>;
processSquare(dsqares, processed); // compiler error! processed is not List<Square>

编译器失败并出现新错误。现在处理列表的类型List&lt;Shape&gt; 与processSquare 的第二个参数List&lt;Square&gt; 不匹配。 使用&lt;? super Square&gt; 通配符告诉编译器给定参数可以是Square 的任何超类 的列表。

void processSquare(List<? extends Square> iSqua, 
                          List<? super Square> oSqua) 

这是示例的完整源代码。有时我发现从一个工作示例开始,然后分解它以查看编译器如何反应,这样更容易学习东西。

package wild;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public abstract class Main {
  // In processing the square, 
  // I'll take for input  any type of List that can PRODUCE (read) squares.
  // I'll take for output any type of List that can ACCEPT (write) squares.
  static void processSquare(List<? extends Square> iSqua, List<? super Square> oSqua) 
  { Square s = iSqua.remove(0); s.doSquare(); oSqua.add(s); }

  static void processCircle(List<? extends Circle> iCirc, List<? super Circle> oCirc) 
  { Circle c = iCirc.remove(0); c.doCircle(); oCirc.add(c); }

  public static void main(String[] args) {
    // Load some inputs
    List<Circle> circles = makeList(new Circle());
    List<DoubleSquare> dsqares = makeList(new DoubleSquare());

    // Collated storage for completed shapes
    List<Shape> processed = new ArrayList<Shape>();

    // Process the shapes
    processSquare(dsqares, processed);
    processCircle(circles, processed);

    // Do post-processing
    for (Shape s : processed)
      s.shapeDone();
  }

  static class Shape { void shapeDone() { System.out.println("Done with shape."); } }
  static class Square extends Shape { void doSquare() { System.out.println("Square!"); } }
  static class DoubleSquare extends Square {}
  static class Circle extends Shape { void doCircle() { System.out.println("Circle!"); } }

  static <T> List<T> makeList(T a) { 
    List<T> list = new LinkedList<T>(); list.add(a); return list; 
  }

}

【讨论】:

  • @ThisIsTheDave 那么 ?
  • 不同意这篇长文。当然,我花了几分钟的时间阅读和消化,但我很感激详细而实用的解释。我现在比以前有了更好的理解。如果读者不准备花几分钟时间来理解一些相当复杂的规则,那么他们可能不会费心去理解它。
  • +1 我读的越多,我就越想给你点赞。你真的应该得到更多的支持。谢谢你这么长的帖子。
【解决方案2】:

要扩展Paul's answer,将 shapeSuper 声明为 List super Shape>,您是说它可以接受任何属于 Shape 超类的对象。对象是形状的超类。这意味着列表中每个元素的公共超类都是 Object。

这就是为什么你必须在 for 循环中使用 Object 类型的原因。就编译器而言,列表可能包含非 Shapes 的对象。

【讨论】:

  • +1 用于实际解释问题所在。添加“?super T”与“?extends T”的讨论,我会再次投票。哦等等……
  • 嘿嘿嘿。我也喜欢这个解释。太糟糕了,我们不能第二次投票了
【解决方案3】:

第 (2.4) 节中的“获取和放置原则”是来自Java Generics and Collections 的真正瑰宝:

Get 和 Put 原则:使用 当你只得到时扩展通配符 结构中的值,使用 super 仅将值放入时的通配符 结构,不要使用通配符 当你得到和放置时。

另外,将类型声明为List&lt;? super Shape&gt; shapeSuper 是一种不好的形式,因为它限制了它的使用。一般来说,我只在方法签名中使用通配符:

public void foo(List<? super Shape> shapeSuper)

【讨论】:

  • 这也被称为 PECS 原则(Effective Java 2nd Ed.)。 “生产者延伸,消费者超级”。如果您接受的参数是生产者,请使用“扩展”。
  • @Julien 你说在我们输入值时使用 super。为什么我们想要将 X 的超类转换为 X,然后将它们放入 X 的列表中?使用 super 有什么好处?为什么不干脆不使用通配符而不管 put/get/put+get 呢?
  • @Pacerier 这可能是一个老问题,但我认为阅读有效的 java (2008) 这本书会帮助你回答你的问题。
【解决方案4】:

尝试将 shapeSuper 声明为 List&lt;Shape&gt;。然后就可以了

for (Shape shape : shapeSuper)

【讨论】:

    【解决方案5】:

    A)

    因为super 表示通用元素的下界类。所以,List&lt;? super Shape&gt; 可以代表List&lt;Shape&gt;List&lt;Object&gt;

    B)

    因为编译器不知道List&lt;? super Shape&gt; 的实际类型是什么。

    您使用shapeSuper.add(new Object()); 添加了一个对象,但编译器只知道List 的泛型类型是Shape 的超类型,但不知道它到底是什么。

    在您的示例中,List&lt;? super Shape&gt; 可能真的是 List&lt;ShapeBase&gt; 强制编译器禁止 shapeSuper.add(new Object()); 操作。

    记住,Generics are not covariant

    【讨论】:

      【解决方案6】:

      (免责声明:我从未将“super”用作通用通配符限定符,因此请谨慎对待...)

      对于(A),实际上你不能添加Shape及其派生词,你只能添加Shape及其祖先。我想也许你想要的是

      List<? extends Shape> shapeSuper = new ArrayList<Shape>();
      

      指定“扩展”意味着 Shape 和任何从 Shape 派生的东西。指定“super”意味着 Shape 和任何 Shape 的后代。

      不确定 (B),除非 Object 不是隐含的。如果您将 Shape 显式声明为 public class Shape extends Object,会发生什么?

      【讨论】:

      • (B) 的问题是你不能将东西添加到使用通配符参数声明的通用容器中。
      • 其实我错了。 add(new Shape()) 工作正常。这个问题比这更棘手。
      【解决方案7】:

      参考上述,我认为这是不正确的:

      通过将 shapeSuper 声明为 List&lt;? super Shape&gt; shapeSuper ,您是在说它 可以接受任何超对象 形状类

      乍一看,这确实很直观,但实际上我不认为它是这样工作的。您不能将 Shape 的任何超类插入到 shapeSuper 中。 superShape 实际上是对 List 的引用,它可能被限制为持有特定(但未指定)的 Shape 超类型。

      让我们想象一下 Shape 实现了 Viewable 和 Drawable。所以在这种情况下,superShape 引用实际上可能指向List&lt;Viewable&gt;List&lt;Drawable&gt;(或者实际上是List&lt;Object&gt;)——但我们不知道是哪一个。如果它实际上是 List&lt;Viewable&gt;,则您不应该将 Drawable 实例插入其中 - 编译器会阻止您这样做。

      实际上,下限结构在使泛型类更灵活方面仍然非常有用。在下面的示例中,它允许我们将定义为包含 Shape 的任何超类的 Set 传递给 addShapeToSet 方法 - 我们仍然可以将 Shape 插入其中:

      public void addShapeToSet(Set<? super Shape> set) {
          set.add(new Shape());
      }
      
      public void testAddToSet() {
          //these all work fine, because Shape implements all of these:
          addShapeToSet(new HashSet<Viewable>());
          addShapeToSet(new HashSet<Drawable>());           
          addShapeToSet(new HashSet<Shape>());           
          addShapeToSet(new HashSet<Object>());
      }
      

      【讨论】:

        猜你喜欢
        • 2018-04-10
        • 1970-01-01
        • 2012-08-30
        • 1970-01-01
        • 2013-04-02
        • 1970-01-01
        • 1970-01-01
        • 2014-12-11
        • 2018-11-20
        相关资源
        最近更新 更多