SamPinheiro
SamPinheiro

Reputation: 143

Type error when dealing with collections of inherited objects

I have the following methods

Rectangle updateRectangle(Rectangle rect);
Collection<Rectangle> updateRectangles(Collection<Rectangle> 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<Square> updateSquare(Collection<Square> 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<Square> cannot be converted to java.util.Collection<Rectangle>

Upvotes: 0

Views: 202

Answers (2)

rzwitserloot
rzwitserloot

Reputation: 103913

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<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!!!!!

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<Rectangle> 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<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!!!!!

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<? extends Whatever>, 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<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!!!!!

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<Object> o = new ArrayList<Object>();
List<? super Rectangle> 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<? super Rectangle> 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<? extends Rectangle> 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<? super Square> 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<? extends Rectangle> and you're good to go.

Upvotes: 2

ttarczynski
ttarczynski

Reputation: 1014

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<? extends Rectangle> updateRectangles(Collection<? extends Rectangle> rects)

For more details on generics wildcards see the docs

Upvotes: 2

Related Questions