The Parameterless Generic Method Antipattern
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 emptyList<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 asjavax.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 abovegetCharSequence()
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); } }
Reference: | The Parameterless Generic Method Antipattern from our JCG partner Lukas Eder at the JAVA, SQL, AND JOOQ blog. |
Great, didn’t know about that. Thank you.