Two Java Type‐System Holes
That is, two bugs in the language specification that allow a program to compile without warnings but to produce a ClassCastException
at runtime on a line with no explicit cast.
Cast to Inner Class of Generic Type
Code
class Outer<E> { Inner inner; class Inner { E e; E getOtherElement(Object other) { if (!(other instanceof Outer.Inner)) { throw new IllegalArgumentException(String.valueOf(other)); } Inner that = (Inner) other; return that.e; } } public static void main(String[] args) { Outer<String> s = new Outer<String>(); s.inner = s.new Inner(); s.inner.e = "hello"; Outer<Integer> i = new Outer<Integer>(); i.inner = i.new Inner(); i.inner.e = 1234; String producesClassCast = s.inner.getOtherElement(i.inner); } }
Output
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String at Outer.main(Outer.java:26)
Explanation
The Java language specification lacks rules about casts to inner classes. Commonly used versions of javac happen to allow us to cast to Inner
— that is, to cast to Outer<E>.Inner
— without a warning. The more honest version of the cast, (Outer<E>.Inner)
, ought likewise to be permitted with a warning; it's rejected outright.
This subtle interaction between generics and inner classes pales in comparison to...
Generic "Overrides" of Static Methods
Code
import java.util.HashSet; import static java.util.Collections.singleton; class Printer { static <E> void print(Iterable<E> input) { for (E e : input) { System.out.println("Unsorted " + e); } } } class SortedPrinter extends Printer { static <E extends Comparable<? super E>> void print(Iterable<E> input) { for (E e : input) { System.out.println("Sorted " + e); } } } class Caller { public static void main(String[] args) { SortedPrinter.print(singleton(1)); SortedPrinter.print(singleton(null)); SortedPrinter.print(singleton(new HashSet<Object>())); } }
Output
Sorted 1 Sorted null Exception in thread "main" java.lang.ClassCastException: java.util.HashSet cannot be cast to java.lang.Comparable at SortedPrinter.print(Caller.java:14) at Caller.main(Caller.java:24)
Explanation
You need two obscure pieces of knowledge to understand this one:
-
At runtime, methods are identified by these four pieces of information:
- return type
- class name
- method name
- argument types
-
Indirect references to static methods are compiled oddly:
The bytecode for a call toclass Base { static void foo() {} } class Derived extends Base { }
Derived.foo()
is "void Derived.foo()
," not "void Base.foo()
." The VM knows to look for the method first inDerived
and then inBase
.
So:
The compiler can see that Collection<HashSet<Object>>
does not
implement Comparable
. Thus, it chooses the more general "inherited" version <E>
print(Iterable<E>)
over the restricted version <E extends Comparable<? super E>>
print(Iterable<E>)
. Nevertheless, per point #2 above, the bytecode it produces still reads "void
SortedPrinter.print(Iterable)
."
The VM sees "void SortedPrinter.print(Iterable)
" and begins its search for void print(Iterable)
in
SortedPrinter
. SortedPrinter
has exactly such a method. The fact that it requires
its input to be Comparable
is unavailable at runtime. The VM chooses that method.
Thus, all three "SortedPrinter.print()
" calls end up really calling
SortedPrinter.print()
, even though the compiler/IDE believes it is
compiling a call to Printer.print()
in the HashSet case. At runtime,
SortedPrinter.print()
implicitly casts each element of its input to
Comparable
("for (E e : input)
"), and the failure to cast HashSet
gives
us a ClassCastException
.
More Fun
This is a hole in the type system, but the fun doesn't stop there. By adding method X, I can cause a call to method Y to become a call to method Z:
import java.util.HashSet; import static java.util.Collections.singleton; class Base { static <E> Object count(Iterable<E> input) { System.out.println("Base"); int count = 0; for (E e : input) { count++; } return count; } } class Sub extends Base { /* static <E> Integer count(Iterable<E> input) { System.out.println("Sub"); int count = 0; for (E e : input) { count++; } return count; }*/ } class Subsub extends Sub { static <E extends Comparable<? super E>> Integer count(Iterable<E> input) { System.out.println("Subsub"); int count = 0; for (E e : input) { count++; } return count; } } class Caller2 { public static void main(String[] args) { Subsub.count(singleton(1)); Subsub.count(singleton(null)); Subsub.count(singleton(new HashSet<Object>())); } }
With Sub.count()
commented out, we get:
Subsub Base Base
With Sub.count()
restored, we get:
Subsub Subsub Subsub <the ClassCastException>
Notice that in no case is Sub.count()
itself called. Its mere presence affects the behavior of the program.
Why? The existence of Sub.count()
makes the compiler write "Integer Subsub.count()
" instead of "Object Subsub.count()
." At runtime, the former resolves to Subsub.count()
; the latter, to Base.count()
.
If you do want Sub.count()
to be called, you must change its return type to Object
. Then we get:
Subsub Sub Sub
Changing the method's return type again changes which method is called. The first method the compiler finds when looking for an appropriate Subsub.count()
is still Sub.count()
, but now it writes "Object Subsub.count()
" instead of "Integer Subsub.count()
." At runtime, the former resolves to the appropriate method.
Lessons
First, the controversial part: When a compiler identifies a call to method X, it should produce code that calls method X, not hypothetical method Y.
Second, programmers should generally avoid "overriding" static methods, most of all when type parameters are involved. <E extends Comparable<? super E>> ImmutableSortedSet.of(E)
and its superclass's <E> ImmutableSet.of(E)
do not play nicely together. Expect some gymnastics in the ImmutableSortedSet
code next release.