A
very interesting question was posted to Stack Overflow and reddit just recently about Java generics. Consider the following method:
<X extends CharSequence> X getCharSequence() {
return (X) "hello";
}
While the unsafe cast seems a bit wonky, and you might guess there’s something wrong here, you can still go ahead and compile the following assignment in Java 8:
Integer x = getCharSequence();
This is obviously wrong, because
Integer
is
final
, and there is thus no possible
Integer
subtype that can also implement
CharSequence
. Yet, Java’s generic type system doesn’t care about classes being
final
final, and it thus infers the intersection type
Integer & CharSequence
for
X
prior to upcasting that type back to
Integer
. From a compiler perspective, all is fine. At runtime:
ClassCastException
While the above seems “obviously fishy”, the real problem lies elsewhere.
It is (almost) never correct for a method to be generic on the return type only
There are exceptions to this rule. Those exceptions are methods like:
class Collections {
public static <T> List<T> emptyList() { ... }
}
This method has no parameters, and yet it returns a generic
List<T>
. Why can it guarantee correctness, regardless of the concrete inference for
<T>
? Because of its semantics. Regardless if you’re looking for an empty
List<String>
or an empty
List<Integer>
, it is possible to provide the same implementation for any of these T, despite erasure, because of the emptiness (and immutable!) semantics.
Another exception is builders, such as
javax.persistence.criteria.CriteriaBuilder.Coalesce<
, which is created from a generic, parameterless method:
<T> Coalesce<T> coalesce();
Builder methods are methods that construct initially empty objects. Emptiness is key, here.
For most other methods, however, this is not true, including the above
getCharSequence()
method. The only guaranteed correct return value for this method is
null
…
<X extends CharSequence> X getCharSequence() {
return null;
}
… because in Java,
null
is the value that can be assigned (and cast) to any reference type. But that’s not the intention of the author of this method.
Think in terms of functional programming
Methods are functions (mostly), and as such, are expected not to have any side-effects. A parameterless function should always return the very same return value. Just like
emptyList()
does.
But in fact, these methods aren’t parameterless. They do have a type parameter
<T>
, or
<X extendds CharSequence>
. Again, because of generic type erasure, this parameter “doesn’t really count” in Java, because short of reification, it cannot be introspected from within the method / function.
So, remember this:
It is (almost) never correct for a method to be generic on the return type only
Most importantly, if your use-case is simply to avoid a pre-Java 5 cast, like:
Integer integer = (Integer) getCharSequence();
Want to find offending methods in your code?
I’m using Guava to scan the class path, you might use something else. This snippet will produce all the generic, parameterless methods on your class path:
import java.lang.reflect.Method;
import java.util.Comparator;
import java.util.stream.Stream;
import com.google.common.reflect.ClassPath;
public class Scanner {
public static void main(String[] args) throws Exception {
ClassPath
.from(Thread.currentThread().getContextClassLoader())
.getTopLevelClasses()
.stream()
.filter(info -> !info.getPackageName().startsWith("slick")
&& !info.getPackageName().startsWith("scala"))
.flatMap(info -> {
try {
return Stream.of(info.load());
}
catch (Throwable ignore) {
return Stream.empty();
}
})
.flatMap(c -> {
try {
return Stream.of(c.getMethods());
}
catch (Throwable ignore) {
return Stream.<Method> of();
}
})
.filter(m -> m.getTypeParameters().length > 0 && m.getParameterCount() == 0)
.sorted(Comparator.comparing(Method::toString))
.map(Method::toGenericString)
.forEach(System.out::println);
}
}
Like this:
Like Loading...
…so, at the end, for legacy(?!?) code, there few options(or i’m wrong?what else?):
1) make class generic:
class GenericProblem
2) using var-arg trick:
public X getCharSequence(Class … ignore)
THX…
i’m in a hurry, i’m going save the kittens ;-P