If B is a subtype of A, why is a collection of B not a subtype of a collection of A?

Asked

Viewed 47 times

2

Dice:

public interface A { ... }

public interface B extends A { ... }

private Set<B> bs = new HashSet<>();

RecebeA qualquerCoisa = new RecebeA(bs);

Why does the last line compile if the constructor RecebeA is declared as:

public RecebeA(Collection<? extends A> as) { ... }

and does not compile if declared as

public RecebeA(Collection<A> as) { ... }

?

If you want to do it the second way new RecebeA(new HashSet<>(bs)); ?

Has better solution?

  • I edited the title, but I don’t know if it’s exactly the "essence" of what you wanted to know...

  • The title is good, is also a doubt, although the essential doubt was why Collection<? extends Interface> works and Collection<Interface> does not. But this question I took in the comments of the reply, thank you.

1 answer

3


This is because inheritance in collections does not work the same way it works with classes. In oracle tutorial it is mentioned that:

In general, if Foo is a subtype (subclass or subinterface) of Bar, and G is some Generic type declaration, it is not the case that G<Foo> is a subtype of G<Bar>.

That is, if B is a subtype of A (be they classes or interfaces), nay it is true that a Set<B> is a subtype of Set<A>. And that doesn’t just apply to Set, but for any generic type. It is counterintuitive because we imagine that the "logical" would be a collection of B should be a subtype of a collection of A, if B is a subtype of A. But it’s not.

The aforementioned tutorial itself gives an example similar to yours:

List<String> ls = new ArrayList<String>();
List<Object> lo = ls; // erro, não compila

After all, if String is a Object, then why a list of String is not a list of Object? Because if the above code compiled, then it would be possible to do this:

lo.add(new Object()); // adicionando um Object que não é uma String
String s = ls.get(0); // ops, não vai retornar uma String

Probably to avoid this type of situation, they decided that it is best not to allow a list of String on a list of Object.


In your case, the same could happen. Suppose I have another sub-interface A:

public interface C extends A { ... }

public class ImplementoC implements C { ... }

public class RecebeA {
    public RecebeA(Collection<A> as) {
        as.add(new ImplementoC()); // isso compila
    }
}

...
Set<B> bs = new HashSet<>();
RecebeA qualquerCoisa = new RecebeA(bs); // isso não compila, mas vamos supor que compilasse
B b = bs.iterator().next(); // aqui seria retornado uma instância de ImplementoC (que é um C, não um B)

That is, if it were possible to pass one Set<B> for the builder who only receives Collection<A>, within it I could add any implementation of A (even if it has no relation of subtype to B), and trying to get the values of Set<B> they would not be instances of B. In the above case, it would be possible to add instances of C in a Set<B>, even though B and C are not subtype of each other.


Interestingly, this doesn’t work either:

public RecebeA(Collection<? extends A> as) {
    as.add(new ImplementoC()); // agora essa linha não compila
}

Set<B> bs = new HashSet<>();
RecebeA qualquerCoisa = new RecebeA(bs); // mas essa compila

Yes, even if the parameter is declared as Collection<? extends A>, i can’t add a C on it. This is because the value received can be a collection of any subclass/sub-interface of A (as an example B, which has no relation of subtype to C, that is, if the line above compiled it would be possible to add a C in a Set<B>).


Borrowing the example of Jon Skeet, to make it a little clearer:

List<Dog> dogs = new ArrayList<Dog>();
// uma lista de cachorros é uma lista de animais, certo? (não, a linha abaixo não compila)
List<Animal> animals = dogs;
// se a linha acima compilasse, seria possível fazer isso:
animals.add(new Cat()); // adicionando um gato na lista de cachorros
Dog dog = dogs.get(0); // ops, não é um cachorro

In the case, Animal would be the interface A, Dog would be the interface B and Cat would be C.

And if I have a class that gets a list of animals:

public class RecebeAnimal {
    public RecebeAnimal(Collection<Animal> c) {
        c.add(new Cat());
    }
}

...
List<Dog> dogs = new ArrayList<>();
RecebeAnimal r = new RecebeAnimal(dogs); // não compila

If the line above compiled, I could add a cat to the dog list.

Now if I change the builder:

public RecebeAnimal(Collection<? extends Animal> c) {
    c.add(new Cat()); // não compila
}

...
List<Dog> dogs = new ArrayList<>();
RecebeAnimal r = new RecebeAnimal(dogs); // compila

The last line compiles because now the Collection may be from any subclass of Animal. But the line with c.add(new Cat()) does not compile, because if compile it would still be possible to add a cat in the list of dogs.

The only thing you can do in the builder is access the elements of the collection as animals: for (Animal a : c) { etc } (just as in your example you could make a for in Collection<? extends A>). Of course you can use it there instanceof and cast to get specific types, but then it already depends on what you need to do.


And how do you solve it? It depends. What exactly RecebeA should receive? A collection of A, of any subtype of A, of only B?

In fact, I think it’s only by having the case concrete to find the best solution. With generic names like this, you can only speculate, but anyway, knowing the limitations and the functioning of wildcards, you can evaluate the requirements and see what is best for your case.

  • The concrete case is this: the class Server instance SocketHandlers. The interface ServerListener stretches SocketHandlerListener. I want to pass the builder of SocketHandler a collection of ServerListeners. Did I violate Liskov’s substitution principle?

  • @Piovezan I didn’t understand anything, what classes are these? : -) Who are the A, B and RecebeA in this story?

  • "Interface A extends B" = "Interface Serverlistener extends Sockethandlerlistener". "Receive" = "Sockethandler". A server has a list of Serverlisteners and Sockethandlers instance, which in turn has Sockethandlerlisteners. I want to pass Serverlisteners to Sockethandler as if they were Sockethandlerlisteners.

  • The object orientation of this must be all wrong :D

  • ServerListener notes the Server, so it has methods onStarted(), onPortBusy(int port), onStopped(), onNewHandler(Sockethandler Handler). Now SocketHandlerListener notes Sockethandler, so it has methods onAuthenticated(Collection<String> tokens), onDisconnect(Collection<String> tokens, String cause). I assumed that anyone interested in Server events is also interested in Sockethandler events.

  • @Piovezan Well, I guess either you change the SocketHandler to get a list List<? extends SocketHandlerListener> (and get it in the builder, then you can pass a List<ServerListener> for him), or you do the same gambit: new SocketHandler(new ArrayList<SocketHandlerListener>(listeners)) - supposing that listeners is the list of Server (one List<ServerListener>).

  • ...which are the two options I proposed in the question. There’s no better way to do it then, right? Just remodeling, I imagine.

  • @Piovezan As it stands, I think these are the options. But if you can review the model, I think it’s a good one. I wonder if ServerListener should really be a subtype of SocketHandlerListener?

  • Yeah, that heritage of sorts isn’t smelling too good. Beauty, answer accepted. :)

  • Just take me one last question, if you’re not already in the answer (then tell me I reread), why Collection<? extends Interface> works and Collection<Interface> nay?

  • @Piovezan Porque Collection<Tipo> indicates that you will receive a collection of Tipo, and Collection<? extends Tipo> indicates that the collection may be of any subtype of Tipo. See the "Bounded Wildcards" section: https://docs.oracle.com/javase/tutorial/extra/generics/wildcards.html - as I said, inheritance in collections works a little different and is often counterintuitive, but I think they did so because of the consequences mentioned in the answer (avoid being possible to add a cat to the dog list, etc)

Show 6 more comments

Browser other questions tagged

You are not signed in. Login or sign up in order to post.