views:

605

answers:

4

I'm confused by the following code:

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

public class GenericsTest<T extends List> {

  public void foo() {
    T var = (T) new LinkedList();
  }

  public static void main(String[] args) {
      GenericsTest<ArrayList> gt1 = new GenericsTest<ArrayList>();
      gt1.foo();
      System.out.println("Done");
  }
}

The runtime type of T appears to be java.util.List, regardless of what Type parameter I passed in to the constructor.

So why is the compiler requiring casting to T when assigning var? Shouldn't it know at compile time that LinkedList is assignable to List?

I understand the code is bogus, and I understand why it worked at runtime even though it looks like it shouldn't. The part that is confusing to me is why is the compiler requiring me to type (T) when doing the assignment? Yet it compiles perfectly well without the bogus cast.

Presumably, the compiler understands erasure. It seems like the compiler should be able to compile the code without the cast as well.

A: 

I assumed you are coming from C#? In that case, Java generics are not C# generics.

Java does not generate a strongly-typed class when you supply a generic type parameter, it simply use the least common denominating type that would work for all the possible generic type parameters combination that could be supplied.

Thus, in that case a List is used since you specify extends List.

chakrit
+1  A: 

That's because of type erasure; at compile time, the generic parameter turns into whatever its lower bound is.

The compiler is trying to tell you that something is wrong. If it did what you think it ought to, you'd get a ClassCastException -- you can't cast a LinkedList to an ArrayList.

Michael Myers
Thanks for the link. That explains why the runtime type is List... and I don't get a ClassCastException. However, presumably the compiler knows that so why does it need to have the cast to (T). Is there any possible way the cast could ever fail?
Eric Rosenberg
+10  A: 

LinkedList is assignable to List, but it may not be assignable to T.

In some ways your test code shouldn't work - it only works because the cast is effectively removed by the compiler as being useless at execution time (due to type erasure). It's useful at compilation time, because it's required you (the developer) to promise the compiler that you're doing something valid which it can't check. As it happens, what you're doing isn't valid, but the compiler has no way of knowing that, and there's no way of checking it at execution time without more information. You can provide that information though:

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

public class GenericsTest<T extends List> {

  Class<T> clazz;

  public GenericsTest(Class<T> clazz) {
    this.clazz = clazz;
  }

  public void foo() {
    T var = clazz.cast(new LinkedList());
  }

  public static void main(String[] args) {
    GenericsTest<ArrayList> gt1 = 
        new GenericsTest<ArrayList>(ArrayList.class);
    gt1.foo();
    System.out.println("Done");
  }
}

This will now fail with an appropriate exception - the type information is still available at execution time, and we're using it to perform the cast in a safe way.

Note that if you compile with suitable warnings, the compiler will tell you that your original code wasn't really checking anything anyway.

For more about this and many other topics, look at the pretty comprehensive Java Generics FAQ.

Jon Skeet
I was trying to link to Angelika Langer's FAQ, but it has this annoying tendency to freeze IE7 _every single time I try it_.
Michael Myers
Weird. Just serves you right for not using Chrome though ;)
Jon Skeet
Thanks for the response. I understand the code was bogus and shouldn't work. What I can't grasp is why the compiler is giving an error if the (T) is removed even though as you pointed out the (T) is pointless and will always work.
Eric Rosenberg
It's not pointless - it's making the developer acknowledge that the types isn't necessarily appropriate. The warning after adding the cast tells the developer that the cast is only a band-aid really; there's still a problem waiting to bite you.
Jon Skeet
+5  A: 

In a comment the poster asks,

However, presumably the compiler knows that so why does it need to have the cast to (T). Is there any possible way the cast could ever fail?

This cast will not fail. But the compiler is warning that this code sets a time bomb ticking to blow up somewhere else with a ClassCastException.

In the example, there's no reason to use generics, since none of the API uses the type variable T. Look at a more realistic application of generics.

   public class GenericsTest<T extends List> {

 3   public T foo() {
 4     T var = (T) new LinkedList();
 5     return var;
 6   }

 8   public static void main(String... argv) {
 9     GenericsTest<ArrayList> gt1 = new GenericsTest<ArrayList>();
10     gt1.foo();
11     System.out.println("Test one okay");
12     ArrayList<?> list = gt1.foo();
13     System.out.println("Test two okay");
14   }

   }

A ClassCastException is thrown at line 12. ClassCastException, without a cast? The calling code is perfectly correct. The invalid cast, the bug, is at line 4, in the invoked method. But the exception is raised at some time and place far distant.

The purpose of Java generics is to assure that code is type-safe. If all of the code was compiled without "unchecked" warnings, the guarantee is that there will be no ClassCastException raised at runtime. However, if a library you depend on was written incorrectly, as was this example, the promise is broken.

erickson