在处理继承对象的集合时出现类型错误。

huangapple go评论82阅读模式
英文:

Type error when dealing with collections of inherited objects

问题

我有以下的方法

Rectangle updateRectangle(Rectangle rect);
Collection<Rectangle> updateRectangles(Collection<Rectangle> rects);

我有两个类:SquareRectangleSquare 扩展 Rectangle

以下的代码可以正常运行,并且具有我想要的预期行为:

Square updateSquare(Square s) {
  return updateRectangle(s);
}

以下的代码会产生类型错误:

Collection<Square> updateSquare(Collection<Square> squares) {
    return updateRectangles(squares);
}

有人可以向我解释为什么Square Rectangle,但是Squares 集合 不是 Rectangles 集合吗?是否有什么方法可以干净地避免这个“不兼容类型”的错误?

精确的错误信息:java: 不兼容的类型: 无法将 java.util.Collection<Square> 转换为 java.util.Collection<Rectangle>

英文:

I have the following methods

Rectangle updateRectangle(Rectangle rect);
Collection&lt;Rectangle&gt; updateRectangles(Collection&lt;Rectangle&gt; rects);

I have two classes: Square and Rectangle. Square extends Rectangle.

The following works and has the desired behavior I want:

Square updateSquare(Square s) {
  return updateRectangle(s);
}

The following gives me a type-error

Collection&lt;Square&gt; updateSquare(Collection&lt;Square&gt; squares) {
    return updateRectangles(squares);
}

Can someone clarify to me why a Square is a Rectangle, but a collection of Squares is not a Collection of Rectangles? Is there something I can do to cleanly get around this "incompatible types" error?

Exact error: java: incompatible types: java.util.Collection&lt;Square&gt; cannot be converted to java.util.Collection&lt;Rectangle&gt;

答案1

得分: 2

这被称为协变性/不变性/逆变性。

矩形列表不是正方形列表,反之亦然:列表是不变的。

普通的数据类型... 是的,在那里所有的正方形也都是矩形:这些是协变的。

那是因为... 从根本上来说是这样的。在这里,让我们去一个想象之地,在独角兽和彩虹之间,我们假装X的列表和Y的列表在数据类型上是协变的,例如正方形的列表也是矩形的列表:

List<Square> squares = new ArrayList<Square>();
squares.add(new Square(20));
List<Rectangle> rectangles = squares;
rectangles.add(new Rectangle(10, 30));

Square s = squares.get(1); // UHOH!!!!!

当我们从正方形列表中检索出我们的正方形,并注意到我们获得的对象实际上并没有相等的边长时,我们的想象世界在烟雾和小矮人的眼泪中崩溃消失。

哎呀!


那么,这里出了什么问题?

add 操作(以及类似的 addAll)实际上是__逆变性的__。关系被颠倒了!如果我将一个正方形添加到一个列表中,那没问题... 只要它是正方形的列表或者任何__超类型__。你可以将正方形添加到正方形的列表、矩形的列表或者对象的列表中。这与 get(int idx) 操作相反,后者是__协变的__:我可以将 listOfSquares.get(0) 的结果分配给 SquareRectangle 或者 Object

在 Java 中,你可以选择自己的变异。来自独角兽之地的上述代码片段在现实世界中无法编译。具体来说,List<Rectangle> rectangles = squares; 这行代码不会编译,因为默认情况下,泛型是不变的,因此正方形的列表与矩形的列表不兼容。

但是你可以控制它,你可以改变它。在这里,让我们引入协变性:

List<Square> squares = new ArrayList<Square>();
squares.add(new Square(20));
List<? extends Rectangle> rectangles = squares;
rectangles.add(new Rectangle(10, 30));

Square s = squares.get(1); // UHOH!!!!!

我们现在引入了协变性:rectangles 对象的定义是:“对矩形的协变”。这是可以的,现在这行代码__编译并且运行得很好__。

但是错误转移到了另一个地方:Java 知道你要求协变性,所有的 add 方法都需要消失,因为这些与协变的概念不兼容。因此是这样的:rectangles.add(new Rectangle(10, 30)) 这行代码__不会编译__。实际上,对于任何 List<? extends Whatever>add 方法都不可调用(除非传递 null,但这是一个与变异大部分无关紧要的偶然情况)。


你甚至可以选择逆变性:

List<Square> squares = new ArrayList<Square>();
squares.add(new Square(20));
List<? super Rectangle> rectangles = squares;
rectangles.add(new Rectangle(10, 30));

Square s = squares.get(1); // UHOH!!!!!

现在我们有了逆变性的矩形列表。正如预期的那样,世界现在完全颠倒过来了:在这个世界里,添加是可以的。但是 get() 是一个问题。

首先,如果我们处于逆变世界中,正方形列表不能分配给矩形列表,所以那行代码不会编译。但是一个对象列表是可以的,所以这行代码会编译:

List<Object> o = new ArrayList<Object>();
List<? super Rectangle> r = o; // 这没问题!
r.add(new Rectangle(10, 20)); // 这也没问题
Rectangle z = r.get(0); // 但这个有问题

现在添加工作得很好,但获取不行。这不是“你根本不能调用get”,而是:get 调用的类型实际上是 java.lang.Object

这是有道理的:你可以将矩形列表或对象列表分配给 List<? super Rectangle> 变量,因为将矩形添加到任一列表中都是可以的。在取值时,我能为你保证的最好结果就是,无论如何,它都是一个对象,所以你得到的就只有这些。

那么,我该如何解决这个问题呢?

奇怪的是,你可能不能解决。听起来 updateRectangles 需要从列表中既“读取”项目又“写入”项目。

如果你要做的只是从列表中读取,而且你希望无论取出什么,至少是一个矩形,那就是协变性,所以你告诉编译器你选择了这个:void readRectangles(Collection<? extends Rectangle> collection) {},你可以很好地将一个正方形的列表传递给这个方法(如果你试图向集合中添加元素,编译器不会让你通过)。

如果你只是要写入,那就选择逆变性:void addSquares(Collection<? super Square> collection) {},你可以在集合上很好地调用 .add(new Square())。你可以传递你的矩形列表或者正方形列表。

但是你既想读取又想写入,所以你只能得到不

英文:

This is called covariance/invariance/contravariance.

A list of rectangles is not a list of squares or vice versa: Lists are invariant.

Plain jane data types.. yes, there all squares are also rectangles: These are covariant.

That's because.. they fundamentally are. Here, let's take a trip to imaginary land and, nestled between unicorns and rainbows, we act as if lists of Xs and lists of Ys are covariant on the data type, e.g. that a list of squares is also a list of rectangles:

List&lt;Square&gt; squares = new ArrayList&lt;Square&gt;();
squares.add(new Square(20));
List&lt;Rectangle&gt; rectangles = squares;
rectangles.add(new Rectangle(10, 30));

Square s = squares.get(1); // UHOH!!!!!

And as we retrieve our square from our list of squares and notice the object we so obtain does not, in fact, have equal sides, our imaginary world crashes and disappears in a poof of smoke and leprecaun tears.

Awwwwww!


So, what's going wrong here?

The add (and add-likes such as addAll) op is in fact contravariant. The relationship is reversed! If I add a square to a list, that's okay.. if it is a list of square or any supertype. You can add squares to a list-o-squares, or a list-o-rectangles, or a list-o-objects. This is in reverse from the get(int idx) operation, which is covariant: I can assign the result of listOfSquares.get(0) to Square, or Rectangle, or Object.

In java, you pick your own variance. The above code snippet from unicorn land does not compile here in the real world. Specifically, the line List&lt;Rectangle&gt; rectangles = squares; won't compile, because by default generics acts invariant, thus making a list of squares not compatible with a list of rectangles.

But you control it, you can change it. Here, let's introduce covariance:

List&lt;Square&gt; squares = new ArrayList&lt;Square&gt;();
squares.add(new Square(20));
List&lt;? extends Rectangle&gt; rectangles = squares;
rectangles.add(new Rectangle(10, 30));

Square s = squares.get(1); // UHOH!!!!!

We have now introduced covariance: The definition of the rectangles object reads: "Covariant on rectangle". This is fine and now this line compiles and runs just fine.

But the error moves: Java knows that given that you demanded covariance, that all add methods need to disappear, because those aren't compatible with the notion of covariance. And so it is: The line rectangles.add(new Rectangle(10, 30)) will not compile. In fact, for any List&lt;? extends Whatever&gt;, the add method is not invokable (unless you pass null, but that's a fluke that's mostly irrelevant to variance).


You can even opt into contravariance:

List&lt;Square&gt; squares = new ArrayList&lt;Square&gt;();
squares.add(new Square(20));
List&lt;? super Rectangle&gt; rectangles = squares;
rectangles.add(new Rectangle(10, 30));

Square s = squares.get(1); // UHOH!!!!!

And now we have a list of contravariant rectangles. As expected, the world is now flipped upside down: Add is fine in this world. But get() is an issue.

First of all, a list of squares is not assignable to a list of rectangles if we're in contravariant world, so that line won't compile. But a list of objects would be, so, this will compile:

List&lt;Object&gt; o = new ArrayList&lt;Object&gt;();
List&lt;? super Rectangle&gt; r = o; // this is fine!
r.add(new Rectangle(10, 20)); // so is this
Rectangle z = r.get(0); // but not this

now add works fine, but get doesn't. Which shows up not as 'you cant call get at all', but as: the type of the 'get' invocation is just java.lang.Object.

This makes sense: You can assign a list of rectangles or a list of objects to a List&lt;? super Rectangle&gt; variable, because adding a rectangle to either of these is fine. When getting values out, best I can do for you is to guarantee you that no matter what, it's an object, so that's all you get.

So, how do I fix this?

Weirdly, you probably can't. It sounds like updateRectangles needs to both 'read' items from the list as well as 'write' items to it.

If all you were going to do is read from the list and you want the guarantee that whatever falls out, it's at least a rectangle, that's covariance, so you tell the compiler you opt into that: void readRectangles(Collection&lt;? extends Rectangle&gt; collection) {} and you can pass a list-o-squares to this method just fine (and if you try to add to the collection, javac won't let you).

If all you were going to do is write, then.. opt into contravariance: void addSquares(Collection&lt;? super Square&gt; collection) {} and you can call .add(new Square()) on the collection just fine. You can pass your list-o-rectangles or your list-o-squares.

But you want both, so all you get is invariance. What you want isn't really possible, not without resorting to unsafe casts or other hackery.

If you describe exactly what updateRectangles actually does, we can help. If you never call .add(), go with Collection&lt;? extends Rectangle&gt; and you're good to go.

答案2

得分: 2

虽然 Square 是 Rectangle 的一个子类,但是从 Java 的类型系统角度来看,一组正方形的集合并不是矩形的集合的子类。Java 期望传递给方法的集合将会持有完全是 Rectangle 类型的实例。若要告诉编译器子类也可以,你需要使用所谓的有界通配符,就像这样:

Collection<? extends Rectangle> updateRectangles(Collection<? extends Rectangle> rects)

有关泛型通配符的更多详细信息,请参阅文档

英文:

Altough Square is a subclass of a Rectangle, Collection of squares is not a subclass of Collection of Rectangle from Java's type system point of view. Java expects, that a collection passed to the method will hold instances of exactly Rectangle type. To tell the compiled that a subclasses will also do, you have to use what it's called a bounded wildcard, like this:

Collection&lt;? extends Rectangle&gt; updateRectangles(Collection&lt;? extends Rectangle&gt; rects)

For more details on generics wildcards see the docs

huangapple
  • 本文由 发表于 2020年7月24日 04:08:19
  • 转载请务必保留本文链接:https://go.coder-hub.com/63062355.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定