Jeff Axelrod
Jeff Axelrod

Reputation: 28188

Confusion with collections of nested generics

Please help me understand why add1() and add4() report errors and why add2() and add3() don't. Specifically, please show examples of undesired consequences if the compiler allowed each of these to compile.

class InnerTypeConfusion {
   interface Animal {}
   class Dog implements Animal {}
   class Room<T> {
      void add(T t) {}
   }

   void add1(Room<? extends Animal> room) {
      // Error: The method add(capture#1-of ? extends Animal) in the type 
      // Room<capture#1-of ? extends Animal> is not applicable for the 
      // arguments (Dog)
      room.add(new Dog());
   }

   void add2(Room<Animal> room) {
      room.add(new Dog());
   }

   class Cage<T> {}

   void add3(Room<Cage<? extends Animal>> room) {
      room.add(new Cage<Dog>());
   }

   void add4(Room<Cage<Animal>> room) {
      // The method add(Cage<Animal>) in the type Room<Cage<Animal>> is not 
      // applicable for the arguments (Cage<Dog>)
      room.add(new Cage<Dog>());
   }
}

Upvotes: 2

Views: 324

Answers (3)

bezmax
bezmax

Reputation: 26142

In the method void add1(Room<? extends Animal> room), you define that the method accepts a Room that holds an Animal. For example, it can be Room<Cat>, or Room<Dog>--even Room<Animal> for holding all types of animals. However, keep in mind that the room has been created outside this method call and you can't make any assumptions about the room type other than that it holds either a specific animal.

add1(new Room<Dog>()); // give the method a room for dogs
add1(new Room<Cat>()); // give the method a room for cats
add1(new Room<Animal>()); // give the method a room for any animal

But once you're inside the method, you can't know specifically which type of room had been passed.

It would be valid to call the method with a room for only birds add1(new Room<Bird>()), as Bird does indeed extend Animal. However in the method body, you are adding a Dog into it. That's why it's invalid, we can't put Dog objects into Room<Bird>. It is a Room of some kind of animals and not a Room of any kind of animals.

If you wanted to write a method that added a dog to a room appropriate for adding dogs (but not limited to just dog-only rooms), you'd write it with signature addDogToRoom(Room<? super Dog> room) per this answer. This method could accept Room<Animal> as well as Room<Dog> and still within the method add new dogs to the room.

As about add4, it's the same but the opposite. With Room<Cage<Animal>> you specify that the method requires a specific room type--a room that allows only cages that hold any kind of Animal. But then you are trying to put a Cage<Dog> into it, a cage that allows only dogs. Therefore, it's invalid again.

Addition regarding comment:

Let's say there are cages designed for containing cats Cage<Cat> and cages designed for containing dogs Cage<Dog>. There are also universal cages, which can contain any kind of animal Cage<Animal>. Those are three different kinds of cages, they can't be substituted for one another, as they have totally different architecture and design.

  • void method(Cage<Dog>) means that the method needs one dog cage.
  • void method(Cage<Animal>) means that the method needs one universal cage.
  • void method(Cage<? extends Animal>) means that the method needs any kind of animal cage. Either dog cage, cat cage or universal cage.

Rooms are another level of abstraction--visualize them as a rooms with cages inside. There can be a room for storing cat cages Room<Cage<Cat>>, a room for storing dog cages Room<Cage<Dog>>, a room for storing universal cages Room<Cage<Animal>> and a room for storing multiple kinds of animal cages Room<Cage<? extends Animal>>. Therefore, the same rules apply:

  • void method(Room<Cage<Dog>>) - room of dog cages
  • void method(Room<Cage<Cat>>) - room of cat cages
  • void method(Room<Cage<Animal>>) - room of animal cages
  • void method(Room<Cage<? extends Animal>>) - room that can contain multiple kinds of animal cages. E.g., the room could simultaneously contain a Cage<Dog>, and a Cage<Animal>.

Now, in add3(Room<Cage<? extends Animal>> room), you request a last kind of room, the one that can contain "all kinds of animal cages". Therefore the room passed to the method can contain or add new dog cages room.add(new Cage<Dog>()) or any other type of cage.

However, to call that method, you would need to first create a new "universal" room (which supports all cages):

Room<Cage<? extends Animal>> room = new Room<Cage<? extends Animal>>();
add3(room);

Giving it a room of dog cages will not work:

// Here we create a room that can contain only dog cages
Room<Cage<Dog>> room = new Room<Cage<Dog>>(); 

// But the method needs a "any kind of animal cage" room
// Therefore we get error during compilation
add3(room); 

If you wanted to write a more flexible method that accepts rooms capable at minimum of holding dog cages, it could look like this:

void add(Room<Cage<? super Dog>> room) {
   room.add(new Cage<Dog>());
   room.add(new Cage<Animal>());
}

Upvotes: 6

Jon Skeet
Jon Skeet

Reputation: 1502106

For add1:

// We don't want to add a Dog to a room of cats...
Room<Cat> cats = new Room<Cat>();
add1(cats);
Cat cat = cats.get(0);

For add4, we don't want to add a Cage<Dog> to a Room of Cage<Animal>... A Cage<Dog> isn't a Cage<Animal>, although it is a Cage<? extends Animal>. It's similar to the first cast, just with one more level of nesting...

Upvotes: 1

Adrian
Adrian

Reputation: 5681

When you use a list of unknown type, marked with ?, the list is pretty much read only. You can insert only null into it. You cannot consume the items in the list. Here you deal with an unknown subclass of Animal

void add1(List<? extends Animal> list) {
   list.add(new Dog());
}

Even though Dog is a subclass of Animal, a List of Dog is not a subclass of a List of Animal. Java doesn't know this automatically so you have to specify it by hand, like you did in add3(..)

void add4(List<Cage<Animal>> list) {
   list.add(new Cage<Dog>());
}

Upvotes: 1

Related Questions