泛型..? 超级 T

13

可能是重复问题:
什么是Java泛型中'super'和'extends'的区别

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及其派生类时,为什么迭代必须是对象?

B)

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

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

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


出于好奇,这样做的好处是什么,而不是定义一个形状接口,然后只创建一个List<Shape>?您仍然可以测试特定的类并进行转换,如果必要的话... - kgrad
7个回答

30
对于你的例子,你可以像Dan和Paul说的那样使用一个普通的List<Shape>;你不需要使用通配符问号语法,比如List<? super Shape>List<? extends Shape>。我认为你的根本问题可能是,“什么时候我会使用问号样式的声明?”(Julien引用的Get and Put Principle是这个问题的一个很好的答案,但我认为除非你在一个例子的背景下看到它,否则它没有太多意义。)以下是我对何时使用通配符的Get and Put Principle的扩展版本。
如果......一个方法有一个泛型类参数Foo<T> readSource 方法从readSource获取T的实例,并且不关心实际检索到的对象是否属于T的子类。
如果......一个方法有一个泛型类参数Foo<T> writeDest
方法将T的实例放入writeDest中,并且不关心writeDest是否还包含T的子类对象。
下面是一个具体示例的演练,说明了通配符背后的思考过程。想象一下,你正在编写一个processSquare方法,该方法从列表中删除一个正方形,处理它,并将结果存储在输出列表中。下面是一个方法签名:
void processSquare(List<Square> iSqua, List<Square> oSqua)
{ Square s = iSqua.remove(0); s.doSquare(); oSqua.add(s); }

现在您需要创建一个DoubleSquares列表,它是Square的扩展,并尝试处理它们:
List<DoubleSquare> dsqares = ... 
List<Square> processed = new ArrayList<Square>;
processSquare(dsqares, processed); // compiler error! dsquares is not List<Square>

编译器出现错误,因为dsquares的类型是List,而processSquare方法的第一个参数类型是List。也许DoubleSquare是Square的子类,但你需要告诉编译器List是List的子类,以便在processSquare方法中使用。使用通配符告诉编译器你的方法可以接受任何Square子类的List。
void processSquare(List<? extends Square> iSqua, List<Square> oSqua)

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

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

编译器出现了一个新错误。现在,被处理的列表List<Shape>的类型与processSquare的第2个参数List<Square>不匹配。使用<? super Square>通配符告诉编译器给定的参数可以是任何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; 
  }

}

1
@ThisIsTheDave 那么 <T extends Obj><? extends Obj> 有什么区别呢? - Pacerier
3
不同意长篇帖子。确实需要我花费几分钟时间来阅读和理解,但是我很感激这篇详细而实用的解释。现在我的理解比之前更好了。如果读者不愿意花费几分钟来理解一些相当复杂的规则,那么他们最好不要尝试去理解它。 - mdma
我读得越多,就越想给你点赞。你真的值得更多的点赞。感谢你写了这么长的帖子。 - Silviu Burcea

23
为了进一步解释 Paul的回答,通过声明shapeSuper为List<? super Shape>,就意味着它可以接受任何Shape的超类对象。Object是Shape的一个超类。这意味着该列表中每个元素的共同超类是Object。
这就是为什么在for循环中必须使用Object类型。就编译器而言,列表可能包含不是Shape的对象。

2
+1 是指实际解释了问题。讨论“? super T”和“? extends T”的区别,我会再次点赞。噢等等... - Michael Myers
呵呵,MMyers。我也喜欢这个解释。太遗憾了,我们不能再投一次赞了。 - Johannes Schaub - litb

18

第2.4节中的“获取和放置原则”是Java Generics and Collections中的一颗真正的明珠:

获取和放置原则:当您只从结构中获取值时,请使用扩展通配符;当您只将值放入结构中时,请使用超级通配符;当您既要获取又要放置时,请不要使用通配符。

alt text

此外,将类型声明为List<? super Shape> shapeSuper有点不好的形式,因为它限制了其使用。通常情况下,我只在方法签名中使用通配符:
public void foo(List<? super Shape> shapeSuper)

3
这也被称为PECS原则(《Effective Java第2版》)。“生产者使用Extends,消费者使用Super”。如果你接受的参数是一个生产者,就使用'extends'。 - Ben Lings
@Julien,你说在我们放置值时要使用super。为什么我们会想将X的超类强制转换为X,然后将它们放入X列表中呢?使用super的优点是什么?无论是put/get/put+get,为什么不直接不使用通配符呢? - Pacerier
@Pacerier,这可能是一个老问题,但我认为阅读《Effective Java》(2008)这本书会帮助你回答你的问题。 - KyelJmD

4
尝试将shapeSuper声明为List<Shape>。然后您可以执行以下操作:
for (Shape shape : shapeSuper)

4

A)

因为 super 表示泛型元素的下限类。所以, List<? super Shape> 可以表示 List<Shape>List<Object>

B)

因为编译器不知道 List<? super Shape> 的实际类型是什么。

你添加了一个对象 shapeSuper.add(new Object());,但编译器只知道 List 的泛型类型是 Shape 的超类型,却不知道具体是哪一个。

在你的例子中,List<? super Shape> 真正可能代表的是 List<ShapeBase>,这迫使编译器禁止 shapeSuper.add(new Object()); 操作。

请记住,泛型不是协变的


2

免责声明:我从未将“超级”用作通配符,所以请谨慎参考此内容...

对于(A),实际上您不能添加Shape及其派生类,只能添加Shape及其祖先。我想您可能想要的是

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

指定 "extends" 表示 Shape 及其所有派生类。指定 "super" 表示 Shape 及其所有继承自 Shape 的类。

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


(B) 的问题在于你无法向使用通配符参数声明的泛型容器中添加元素。 - Michael Myers
实际上,我错了。add(new Shape())运行良好。问题比那复杂一些。 - Michael Myers

0
关于上述内容,我认为这不正确:
通过声明shapeSuper为List<? super Shape> shapeSuper,你是在说它可以接受任何Shape的超类对象。
乍一看似乎是这样,但实际上我认为并不是这样。你不能将任何Shape的超类插入到shapeSuper中。superShape实际上是一个引用,它可能被限制为持有Shape的特定(但未指定)超类型的List。
让我们想象一下,Shape实现了Viewable和Drawable。所以在这种情况下,superShape引用实际上可能指向List<Viewable>List<Drawable>(或者确实是List<Object>)- 但我们不知道哪一个。如果它实际上是List<Viewable>,那么你就不能将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>());
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接