Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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)

like image 686
Kyrylo Zapylaiev Avatar asked May 24 '12 19:05

Kyrylo Zapylaiev


People also ask

How do I restrict a generic type in Java?

Whenever you want to restrict the type parameter to subtypes of a particular class you can use the bounded type parameter. If you just specify a type (class) as bounded parameter, only sub types of that particular class are accepted by the current generic class.

Do collections use generics in Java?

The generic collections are introduced in Java 5 Version. The generic collections disable the type-casting and there is no use of type-casting when it is used in generics. The generic collections are type-safe and checked at compile-time. These generic collections allow the datatypes to pass as parameters to classes.

Why primitive data types are not allowed in Java generics?

So, anything that is used as generics has to be convertable to Object (in this example get(0) returns an Object ), and the primitive types aren't. So they can't be used in generics.

What is generic type erasure?

Type erasure is a process in which compiler replaces a generic parameter with actual class or bridge method. In type erasure, compiler ensures that no extra classes are created and there is no runtime overhead.


2 Answers

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
like image 99
Robert Avatar answered Sep 25 '22 11:09

Robert


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.

like image 28
Judge Mental Avatar answered Sep 22 '22 11:09

Judge Mental