Reputation: 199
I can't figure out why I'm not getting errors in my Java code. I have a class which uses a generic type:
import java.util.*; // For ArrayList
public class Hat<T>
{
public ArrayList<T> convert(String s)
{
T t = (T) s; // Cast happens here
ArrayList<T> list = new ArrayList<T>();
list.add(t);
return list;
}
}
Then, I execute some code which I think should create an error:
Hat<Integer> h = new Hat<Integer>();
ArrayList<Integer> iList = h.convert("hello");
What this does is creates an ArrayList of Integers, that, somehow, has a String as an element! This does not throw any errors on runtime, not even if you print the ArrayList (it prints "[hello]").
I would've expected an error to be thrown from the "convert" method. Why does this not happen, and is it possible to make it happen? Interestingly, it happens when I try to get the element back from the ArrayList as an Integer, but the error doesn't come from the "convert" method.
Upvotes: 0
Views: 61
Reputation: 14025
In Java, generics are only used at compile-time; they are "erased" after the type-checker validates the program and have no effect on the program's execution. In particular, at runtime there's no difference between an ArrayList<Integer>
and an ArrayList<String>
(or an ArrayList
of anything else, for that matter). After typechecking is complete, your program is erased, and the program that executes is equivalent to:
public class Hat
{
public ArrayList convert(String s)
{
Object t = s;
ArrayList list = new ArrayList();
list.add(t);
return list;
}
}
Hat h = new Hat();
ArrayList iList = h.convert("hello");
which behaves the way you observed.
So the question is, why does this program typecheck when it obviously produces a bad value that claims to be an ArrayList<Integer>
but contains strings? Shouldn't the type system reject programs like that?
Well, it does, except that there's a big loophole: unchecked casts. When you do a cast to a type that involves a generic -- in your case, the line T t = (T) s;
-- Java doesn't have anything at runtime that it could use to test if the cast is valid, due to erasure. The Java designers could have just disallowed that kind of cast, in which case your program would fail to compile.
They didn't do it that way, though. Instead, they chose to allow casts that involve generics and trust that the programmer who wrote the cast was smarter than the compiler and knew the cast would work out. If you use one of these casts, though, all bets are off, and the type system can end up, as you discovered, with ArrayList<Integer>
s that actually contain strings. So to warn you that you need to be careful, they had the compiler
but to issue an "unchecked cast" warning whenever you write such a cast, reminding you that there's a suspicious cast and it's up to you to prove that it's correct. In codebases I've worked on, unchecked casts need to be annotated with @SuppressWarning
and a comment describing why the cast is always valid.
So what if you want to deal with unchecked casts and you'd rather issue a runtime check? In that case you're going to have to program the runtime check yourself. You can often do this with Class
objects. In your case, you could add an extra Class
parameter to your Hat
constructor that represents the class you expect T
to be, and use it to make a typesafe cast that's checked at runtime:
public class Hat<T>
{
private final Class<? extends T> expectedClass;
public Hat(Class<? extends T> expectedClass)
{
this.expectedClass = expectedClass;
}
public ArrayList<T> convert(String s)
{
T t = expectedClass.cast(s); // This cast will fail at runtime if T isn't String
ArrayList<T> list = new ArrayList<T>();
list.add(t);
return list;
}
}
Then your callsite would need to change to:
Hat<Integer> h = new Hat<Integer>(Integer.class);
ArrayList<Integer> iList = h.convert("hello"); // throws
Upvotes: 3