1

I have a method that is supposed to return an instance of a given collection class with the given entries. The implementation is shown below.

public static <E, C extends Collection<E>> C
         initalizeAndAddToCollection(Class<C> clazz, E... entries) {
    //Collection object
    Collection<E> collection;
    //try to invoke default constructor of C
    try {
        collection = clazz.getDeclaredConstructor().newInstance();
    } catch (InstantiationException | IllegalAccessException |
             InvocationTargetException | NoSuchMethodException e) {
        throw new RuntimeException();
    }
    //Add elements to collection
    for (E entry: entries)
        collection.add(entry);
    return (C) collection;
}

The problem is that the following code runs even though a double should not be able to be stored in a list of type Integer

 //Create and instantiate an ArrayList with element 1.0
 ArrayList<Integer> list = initalizeAndAddToCollection(ArrayList.class, 1.0);
 System.out.print(list.get(0));

Why does this code run, and how do I make it so that it results in either a compile or runtime error?

Edit: I noticed list.get(0).getClass() does generate an exception, but I am not sure why either (or why the previous code does not).

Debu Shinobi
  • 655
  • 8
  • 14
Lindstorm
  • 771
  • 1
  • 5
  • 19

2 Answers2

2

In Java, generics are a compile-time only feature because they're implemented via type erasure. So while you may think you're creating an ArrayList<Integer> at runtime with your newInstance call, you're really just creating an ArrayList.

In short, reflection breaks type safety for generics in Java.

Powerlord
  • 82,184
  • 16
  • 119
  • 164
  • On an unrelated side note, this same type of thing would throw an exception in C# because C# generics act more like C++ templates. – Powerlord May 11 '19 at 03:50
  • If I am really creating an `ArrayList` instead of `ArrayList`, shouldn't that cause an exception at runtime when I point a reference of the type `ArrayList` to an object of the type `ArrayList`?. I noticed `list.get(0).getClass()` does generate a exception, but why not this code? – Lindstorm May 11 '19 at 03:58
  • `list.get(0).getClass()` should be returning the class for an `Integer` because of something else I didn't mention: When you're dealing with a generic return type, the compiler inserts a cast. Meaning that `list.get(0)` is actually `((Integer) list.get(0))` – Powerlord May 11 '19 at 04:06
  • From what I understand, `list.get(0).getClass()` is actually `((Integer) list.get(0)).getClass()` but `list.get(0)` by itself has no implicit cast (and hence no casting exception). Is there a reason why the latter does not have a implicit cast? – Lindstorm May 11 '19 at 04:53
1

This code gives a warning when I compile it. The warning says:

Note: MyTest.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

And in fact, when you do what it produces 4 warning messages:

MyTest.java:5: warning: [unchecked] Possible heap pollution from parameterized vararg type E
        initalizeAndAddToCollection(Class<C> clazz, E... entries) {
                                             ^
  where E,C are type-variables:
    E extends Object declared in method <E,C>initalizeAndAddToCollection(Class<C>,E...)
    C extends Collection<E> declared in method <E,C>initalizeAndAddToCollection(Class<C>,E...)


MyTest.java:18: warning: [unchecked] unchecked cast
        return (C) collection;
                   ^
  required: C
  found:    Collection<E>
  where E,C are type-variables:
    E extends Object declared in method <E,C>initalizeAndAddToCollection(Class<C>,E...)
    C extends Collection<E> declared in method <E,C>initalizeAndAddToCollection(Class<C>,E...)
MyTest.java:23: warning: [unchecked] unchecked method invocation: method initalizeAndAddToCollection in class MyTest is applied to given types
             initalizeAndAddToCollection(ArrayList.class, 1.0);
                                        ^
  required: Class<C>,E[]
  found: Class<ArrayList>,double
  where C,E are type-variables:
    C extends Collection<E> declared in method <E,C>initalizeAndAddToCollection(Class<C>,E...)
    E extends Object declared in method <E,C>initalizeAndAddToCollection(Class<C>,E...)
MyTest.java:23: warning: [unchecked] unchecked conversion
             initalizeAndAddToCollection(ArrayList.class, 1.0);
                                        ^
  required: ArrayList<Integer>
  found:    ArrayList
4 warnings

These warnings explain why this seemingly incorrect code is compiling. You are doing things that the compiler would tell you are incorrect.


Why does this code run.

Because it is not actually breaking runtime type-safety.

  • Type erasure means that the collection is actually storing references to Object.

  • When you then do System.out.print(list.get(0)); the parameter type for the print call is Object. That means no implicit cast to Integer is needed.

and how do I make it so that so that it results in either a compile or runtime error?

If you need a compile time error, tell the compiler to treat warnings as errors. (Or check for warnings in the compilation output.)

If you need a runtime error, you need to add some explicit runtime type checks probably in your initalizeAndAddToCollection method.

Stephen C
  • 632,615
  • 86
  • 730
  • 1,096
  • Why does the compiler not implicitly cast `list.get(0)` but seems to implicitly casts `list.get(0).getClass()` (the latter code generates a `ClassCastException`). Wouldn't the same argument apply if I did `System.out.print(list.get(0).getClass())` as this call would return an `Object`? – Lindstorm May 11 '19 at 04:56
  • Not clear. But it doesn't matter. You shouldn't write code like this, and your shouldn't ignore warnings about unchecked conversions ... without examining them carefully. – Stephen C May 11 '19 at 05:57