20

I have the following class (simplified but still a working example):

class Test<T> {
    List<T> l = new ArrayList<>();

    public Test() {
    }

    public void add(Object o) {
        l.add((T)o);
    }
}

And the test code:

Test<Double> t = new Test<>();
t.add(1);
t.add(1.2);
t.add(-5.6e-2);
t.add("hello");

Everything is working fine, and that's not what I was expecting. Shouldn't the add method throw a ClassCastException? If I add a get method that's more or less the same thing:

    public T get(int i) {
        return l.get(i);
    }
.../...
t.get(1);             // OK.
t.get(3);             // OK (?)
Double d = t.get(3);  // throws ClassCastException

Why is it only on variable assignment that the exception is thrown? How can I enforce type consistency if the (T) cast doesn't work?

gregseth
  • 12,952
  • 15
  • 63
  • 96
  • 12
    When you compiled, did you get any warnings about "unchecked casts" by any chance? (You should have done...) – Jon Skeet Aug 12 '15 at 09:22
  • 11
    What is so bad about `public void add(T o);` ? – Florian Schaetz Aug 12 '15 at 09:24
  • 3
    To answer the other part of the question: due to type erasure there is no way for `ArrayList` to check and know whether what you put in the list is compatible with the type parameter you set. – biziclop Aug 12 '15 at 09:27

3 Answers3

21

Shouldn't the add method throw a ClassCastException?

No, it shouldn't (although I wish it did). Long story short, Java implementation of generics discards type information after compiling your code, so List<T> is allowed to take any Object, and the cast inside your add method is not checked.

Why is it only on variable assignment that the exception is thrown?

Because the cast to Double there is inserted by the compiler. Java compiler knows that the return type of get is T, which is Double, so it inserts a cast to match the type of the variable d, to which the result is being assigned.

Here is how you can implement a generic-safe cast:

class Test<T> {
    private final Class<T> cl;
    List<T> l = new ArrayList<>();

    public Test(Class<T> c) {
        cl = c;
    }

    public void add(Object o) {
        l.add(cl.cast(o));
    }
}

Now the cast is performed by a Class<T> object, so you will get a ClassCastException on an attempt to insert an object of an incorrect type.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
8

For the completeness of this resource, here's the difference in compiled bytecode between a cast to a generic:

public void add(java.lang.Object);
  Code:
     0: aload_0
     1: getfield      #4                  // Field l:Ljava/util/List;
     4: aload_1
     5: invokeinterface #7,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
    10: pop
    11: return

And an explicit cast to a Double with no generics:

public void add(java.lang.Object);
  Code:
     0: aload_0
     1: getfield      #4                  // Field l:Ljava/util/List;
     4: aload_1
     5: checkcast     #7                  // class java/lang/Double
     8: invokeinterface #8,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
    13: pop
    14: return

You can see that the version with generics doesn't perform the checkcast instruction at all (thanks to type erasure, so you shouldn't expect an exception when giving it data with an unmatched class. It's unfortunate that this isn't more strictly enforced, but this makes sense as generics are for making stricter compile-time type checks, and aren't much help at runtime due to type erasure.

Java will check the types of function arguments to see if there is a type match, or if a type promotion can be performed. In your case, String is the type of the argument, and that can be promoted to Object, which is the extent of the compile-time type checks that ensure that function call works.

There are a few options, and dasblinkenlight's solution is probably the most elegant. (You may not be able to change the method signature, say, if you are overriding an inherited add method, or plan on passing down the add method, etc).

Another option that may help is using a bounded type parameter instead of an unbounded one. Unbounded type parameters are completely lost after compilation due to type erasure, but using a bounded type parameter will replace instances of the generic type with that/those it must extend.

class Test<T extends Number> {

Of course, T is not truly generic at this point, but using this class definition will enforce types at runtime since the cast will be checked against the Number superclass. Here's the bytecode to prove it:

public void add(java.lang.Object);
  Code:
     0: aload_0
     1: getfield      #4                  // Field l:Ljava/util/List;
     4: aload_1
     5: checkcast     #7                  // class java/lang/Number
     8: invokeinterface #8,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
    13: pop
    14: return

This class definition generates the desired ClassCastException when trying to add the string.

Purag
  • 16,941
  • 4
  • 54
  • 75
7

As an alternative solution you can use Collections.checkedList:

class Test<T> {
    List<T> l;

    public Test(Class<T> c) {
        l = Collections.checkedList(new ArrayList<T>(), c);
    }

    public void add(Object o) {
        l.add((T) o);
    }
}

This way you will get the following exception:

Exception in thread "main" java.lang.ClassCastException: Attempt to insert 
  class java.lang.Integer element into collection with element type class java.lang.Double
    at java.util.Collections$CheckedCollection.typeCheck(Collections.java:3037)
    at java.util.Collections$CheckedCollection.add(Collections.java:3080)
    at Test.add(Test.java:13)
Tagir Valeev
  • 97,161
  • 19
  • 222
  • 334