Kyrylo Zapylaiev
Kyrylo Zapylaiev

Reputation: 781

Java Generics: adding wrong type in collection

Who could me explain this?

I have these couple of classes:

abstract class Animal {
    public void eat() {
        System.out.println("Animal is eating");
    }
}

class Dog extends Animal {
    public void woof() {
        System.out.println("woof");
    }
}

class Cat extends Animal {
    public void meow() {
        System.out.println("meow");
    }
}

And this is the action:

import java.util.ArrayList;
import java.util.List;

public class TestClass {

    public static void main(String[] args) {
        new TestClass().go();
    }

    public void go() {
        List<Dog> animals = new ArrayList<Dog>();
        animals.add(new Dog());
        animals.add(new Dog());
        doAction(animals);
    }

    public <T extends Animal> void doAction(List<T> animals) {

        animals.add((T) new Cat()); // why is it possible? 
                                    // Variable **animals** is List<Dog>, 
                                    // it is wrong, that I can add a Cat!

        for (Animal animal: animals) {
            if (animal instanceof Cat) {
                ((Cat)animal).meow();
            }
            if (animal instanceof Dog) {
                ((Dog)animal).woof();
            }
        }
    }
}

This example compile without errors, and output is:

woof
woof
meow

But how can I add in list of Dog a Cat? And how the Cat is casted to Dog?

I use: java version "1.6.0_24". OpenJDK Runtime Environment (IcedTea6 1.11.1) (6b24-1.11.1-4ubuntu3)

Upvotes: 8

Views: 2750

Answers (6)

Robert
Robert

Reputation: 8619

Ok here's the deal with generics (anything that uses casting hackery might not be safe at runtime, because generics work by erasure):

You can assign a subtype parameterised the same way e.g

List<Animal> l = new ArrayList<Animal>();

and you can add items that are the type of this parameter or its subclasses e.g

l.add(new Cat());
l.add(new Dog());

but you can only get out the type of the parameter:

Animal a = l.get(0);
Cat c = l.get(0); //disallowed
Dog d = l.get(1); //disallowed

Now, you can use a wild card to set an upper bound on the parameter type

List<? extends Animal> l = new ArrayList<Animal>();
List<? extends Animal> l = new ArrayList<Cat>();
List<? extends Animal> l = new ArrayList<Dog>();

But you can't add new items to this list

l.add(new Cat()); // disallowed
l.add(new Dog()); // disallowed

In your case you have a List<T> so it has a method add(T t) so you can add if you cast to T. But T has type bounded above by Animal so you shouldn't even be trying to add to this list, but it is treated as a concrete type and that's why it allows the cast. However this may throw a ClassCastException.

And you can only retrieve items that are the upper bound type

Animal a = l.get(0);
Cat c = l.get(0); //disallowed
Dog d = l.get(1); //disallowed

Or you can set the lower bound parameter type

List<? super Animal> l1 = new ArrayList<Object>();
List<? super Animal> l1 = new ArrayList<Animal>();
List<? super Cat> l2 = new ArrayList<Animal>();
List<? super Cat> l2 = new ArrayList<Cat>();
List<? super Dog> l3 = new ArrayList<Animal>();
List<? super Dog> l3 = new ArrayList<Dog>();

And you can add objects that are subtypes of the lower bound type

l1.add(new Cat());
l1.add(new Dog());
l1.add(new Object()); //disallowed

But all objects retrieved are of type Object

Object o = l1.get(0);
Animal a = l1.get(0); //disallowed
Cat c = l2.get(0); //disallowed
Dog d = l3.get(0); //disallowed

Upvotes: 8

Joseph Elcid
Joseph Elcid

Reputation: 877

Your function doAction is parametrized by a type T that extends the class Animal. So it can be either an object of type Dog or Cat.
You should also note the difference between formal parameter and effective parameter.
The formal parameter is the one you use when defining your method, which in your case is List <T> (using it as a shortcut).
The effective parameter is the one you "give" to your method when call it, here List<Dog>.
.
Lets take a look at animals.add((T) new Cat()) in doAction(). animals is a list which elements are of type T which is either Dog or Cat, so there is no mistake, since that's the type of the formal parameter.
So that's the reason. It is one of the benefits of using parametrize classes.

Upvotes: 0

Judge Mental
Judge Mental

Reputation: 5239

Do not expect generics to perform runtime type checking. During compilation, Java performs all the type inference, instantiates all the types, ... and then erases all trace of generic types from the code. At runtime, the type is List, not List< T > or List< Dog >.

The main question, why it allows you to cast new Cat() to type T extends Animal, with only a warning about unchecked conversions, is valid. Certain unsound features of the type system make legalizing such dubious casts necessary.

If you want the compiler to prevent the addition of anything to the list, you should use a wildcard:

public void doAction(List< ? extends Animal > animals) {
    animals.add(new Cat()); // disallowed by compiler
    animals.add((Animal)new Cat()); // disallowed by compiler

    for (Animal animal: animals) {
        if (animal instanceof Cat) {
            ((Cat)animal).meow();
        }
        if (animal instanceof Dog) {
            ((Dog)animal).woof();
        }
    }
}

P.S. The dubious downcasts in the loop body are a perfect example of how lame Java is for disjoint sum (variant) types.

Upvotes: 2

Marko Topolnik
Marko Topolnik

Reputation: 200306

Suffice it to say that there is such a T for which the cast can succeed. As for compiled code, it will be exactly the same as if you wrote animals.add((Animal)new Cat());

Upvotes: 0

tjg184
tjg184

Reputation: 4686

This is related to type erasure. The type is not preserved at runtime. Really, List becomes List of Type Object at runtime. This is why you're getting a compiler warning or should be on animals.add((T) new Cat()); At compile time, Cat does extend animal which is of type T. However, it cannot enforce that a Dog is in the list at that time, therefore the compiler warning.

Upvotes: 1

ControlAltDel
ControlAltDel

Reputation: 35106

Generics only work for compile time safety. In your case, how can the compiler know that something bad will happen? It assumes your type definitions, and proceeds off of that. To do more would mean much more elaborate work for the compiler, but there are extended static checkers and other tools that could catch this pre-runtime.

Upvotes: 0

Related Questions