I had recently blogged about funny issues that arise when overloading API methods with generics involved:
https://blog.jooq.org/overload-api-methods-with-care/
I promised a sequel as I have encountered more trouble than that, so here it is.
The trouble with generics and varargs
Varargs are another great feature introduced in Java 5. While being merely syntactic sugar, you can save quite some lines of code when passing arrays to methods:
// Method declarations with or without varargs
public static String concat1(int[] values);
public static String concat2(int... values);
// The above methods are actually the same.
String s1 = concat1(new int[] { 1, 2, 3 });
String s2 = concat2(new int[] { 1, 2, 3 });
// Only, concat2 can also be called like this, conveniently
String s3 = concat2(1, 2, 3);
That’s well-known. It works the same way with primitive-type arrays as with Object[]. It also works with T[] where T is a generic type!
// You can now have a generic type in your varargs parameter:
public static <T> T[] array(T... values);
// The above can be called "type-safely" (with auto-boxing):
Integer[] ints = array(1, 2, 3);
String[] strings = array("1", "2", "3");
// Since Object could also be inferred for T, you can even do this:
Object[] applesAndOranges = array(1, "2", 3.0);
The last example is actually already hinting at the problem. If T does not have any upper bound, the type-safety is gone, completely. It is an illusion, because in the end, the varargs parameter can always be inferred to “Object…”. And here’s how this causes trouble when you overload such an API.
// Overloaded for "convenience". Let's ignore the compiler warning
// caused when calling the second method
public static <T> Field<T> myFunction(T... params);
public static <T> Field<T> myFunction(Field<T>... params);
At first, this may look like a good idea. The argument list can either be constant values (T…) or dynamic fields (Field…). So in principle, you can do things like this:
// The outer function can infer Integer for <T> from the inner
// functions, which can infer Integer for <T> from T...
Field<Integer> f1 = myFunction(myFunction(1), myFunction(2, 3));
// But beware, this will compile too!
Field<?> f2 = myFunction(myFunction(1), myFunction(2.0, 3.0));
The inner functions will infer Integer and Double for <T>. With incompatible return types Field<Integer> and Field<Double>, the “intended” method with the “Field<T>…” argument does not apply anymore. Hence method one with “T…” is linked by the compiler as the only applicable method. But you’re not going to guess the (possibly) inferred bound for <T>. These are possible inferred types:
// This one, you can always do:
Field<?> f2 = myFunction(myFunction(1), myFunction(2.0, 3.0));
// But these ones show what you're actually about to do
Field<? extends Field<?>> f3 = // ...
Field<? extends Field<? extends Number>> f4 = // ...
Field<? extends Field<? extends Comparable<?>>> f5 = // ...
Field<? extends Field<? extends Serializable>> f6 = // ...
The compiler can infer something like Field<? extends Number & Comparable<?> & Serializable> as a valid upper bound for <T>. There is no valid exact bound for <T>, however. Hence the necessary <? extends [upper bound]>.
Conclusion
Be careful when combining varargs parameters with generics, especially in overloaded methods. If the user correctly binds the generic type parameter to what you intended, everything works fine. But if there is a single typo (e.g. confusing an Integer with a Double), then your API’s user is doomed. And they will not easily find their mistake, as no one sane can read compiler error messages like this:
Test.java:58: incompatible types
found : Test.Field<Test.Field<
? extends java.lang.Number&java.lang.Comparable<
? extends java.lang.Number&java.lang.Comparable<?>>>>
required: Test.Field<java.lang.Integer>
Field<Integer> f2 = myFunction(myFunction(1),
myFunction(2.0, 3.0));
Like this:
Like Loading...