On type erasure
Java's generics are non-reified. A List<String>
and a List<Integer>
are different types at compile time, but both types are erased to simply List
at run-time. This means that by circumventing the compile-time checks, you can insert an Integer
into List<String>
at run-time, which by itself may not generate ClassCastException
. Here's an example:
List<String> names = new ArrayList<String>();
List raw = names; // generates compiler warning about raw type!
raw.add((Integer) 42); // does not throw ClassCastException! (yet!)
// but here comes trouble!
for (String s : names) {
// Exception in thread "main" java.lang.ClassCastException:
// java.lang.Integer cannot be cast to java.lang.String
}
Note that you'd have to deliberately circumvent the compile-time check to violate the generic type invariant: the compiler will do its best to ensure that List<String>
will indeed contain only String
, and will generate as many warnings and errors as necessary to enforce this.
On checked collections
Sometimes we want to enforce the type safety at run-time. For most scenarios, the checked collection wrappers from java.util.Collections
can facilitate this behavior. From the documentation:
<E> Collection<E> checkedCollection(Collection<E> c, Class<E> type)
Returns a dynamically typesafe view of the specified collection. Any attempt to insert an element of the wrong type will result in an immediate ClassCastException
. Assuming a collection contains no incorrectly typed elements prior to the time a dynamically typesafe view is generated, and that all subsequent access to the collection takes place through the view, it is guaranteed that the collection cannot contain an incorrectly typed element.
The generics mechanism in the language provides compile-time (static) type checking, but it is possible to defeat this mechanism with unchecked casts. Usually this is not a problem, as the compiler issues warnings on all such unchecked operations. There are, however, times when static type checking alone is not sufficient. For example, suppose a collection is passed to a third-party library and it is imperative that the library code not corrupt the collection by inserting an element of the wrong type.
Another use of dynamically typesafe views is debugging. Suppose a program fails with a ClassCastException
, indicating that an incorrectly typed element was put into a parameterized collection. Unfortunately, the exception can occur at any time after the erroneous element is inserted, so it typically provides little or no information as to the real source of the problem. If the problem is reproducible, one can quickly determine its source by temporarily modifying the program to wrap the collection with a dynamically typesafe view. For example, this declaration:
Collection<String> c = new HashSet<String>();
may be replaced temporarily by this one:
Collection<String> c = Collections.checkedCollection(
new HashSet<String>(), String.class);
Running the program again will cause it to fail at the point where an incorrectly typed element is inserted into the collection, clearly identifying the source of the problem.
Here's a modification of the previous snippet:
List<String> names = Collections.checkedList(
new ArrayList<String>(), String.class
);
List raw = names; // generates compiler warning about raw type!
raw.add((Integer) 42); // throws ClassCastException!
// Attempt to insert class java.lang.Integer element into collection
// with element type class java.lang.String
Note that as a "bonus", a Collections.checkedList
will throw NullPointerException
at run-time on an attempt to add(null)
.
On Class.isAssignableFrom
Unfortunately the behavior we want in this scenario is not supported by a Collections.checkedList
: you can use it to ensure that only java.lang.Class
instances can be added at run-time, but it will not ensure that the given Class
object is a subclass of another Class
.
Fortunately the Class.isAssignableFrom(Class)
method allows you to make this check, but you'd have to write your own checked List
wrapper to enforce this. The idea is illustrated here in a static
helper method instead of a full List
implementation:
static void add(List<Class<?>> list, Class<?> base, Class<?> child) {
if (base.isAssignableFrom(child)) {
list.add(child);
} else {
throw new IllegalArgumentException(
String.format("%s is not assignable from %s",
base.getName(),
child.getName()
)
);
}
}
Now we have:
List<Class<?>> list = new ArrayList<Class<?>>();
add(list, CharSequence.class, String.class); // OK!
add(list, CharSequence.class, StringBuffer.class); // OK!
add(list, CharSequence.class, StringBuilder.class); // OK!
add(list, CharSequence.class, Integer.class); // NOT OK!
// throws IllegalArgumentException:
// java.lang.CharSequence is not assignable from java.lang.Integer