Core Java

Overload API methods with care

Overloading methods is a strong concept in API design, especially when your API is a fluent API or DSL (Domain Specific Language).

This is the case for jOOQ, where you often want to use the exact same method name for various means of interaction with the library.







Example: jOOQ Conditions

package org.jooq;



public interface Condition {



    // Various overloaded forms of the "AND" operation:

    Condition and(Condition other);

    Condition and(String sql);

    Condition and(String sql, Object... bindings);



    // [...]

}

All of these methods connect two conditions with each other using an “AND” operator. Ideally, the implementations depend on each other, creating a single point of failure. This keeps things DRY:

package org.jooq.impl;



abstract class AbstractCondition implements Condition {



    // The single point of failure

    @Override

    public final Condition and(Condition other) {

        return new CombinedCondition(

            Operator.AND, Arrays.asList(this, other));

    }



    // "Convenience methods" delegating to the other one

    @Override

    public final Condition and(String sql) {

        return and(condition(sql));

    }



    @Override

    public final Condition and(String sql, Object... bindings) {

        return and(condition(sql, bindings));

    }

}

The trouble with generics and overloading

When developing with Eclipse, the Java 5 world seems more shiny than it really is. Varargs and generics were introduced as syntactic sugar in Java 5. They don’t really exist in that way in the JVM. That means, the compiler has to link method invocations correctly, inferring types if needed, and creating synthetic methods in some cases. According to the JLS (Java Language Specification), there is a lot of ambiguity when varargs/generics are employed in overloaded methods.

Let’s elaborate on generics:

A nice thing to do in jOOQ is to treat constant values the same as fields. In many places, field arguments are overloaded like this:

// This is a convenience method:

public static <T> Field<T> myFunction(Field<T> field, T value) {

    return myFunction(field, val(value));

}



// It's equivalent to this one.

public static <T> Field<T> myFunction(Field<T> field, Field<T> value) {

    return MyFunction<T>(field, value);

}

The above works very well in most of the cases. You can use the above API like this:

Field<Integer> field1  = //...

Field<String>  field2  = //...



Field<Integer> result1 = myFunction(field1, 1);

Field<String>  result2 = myFunction(field2, "abc");

But the trouble arises when <T> is bound to Object!

// While this works...

Field<Object>  field3  = //...

Field<Object>  result3 = myFunction(field3, new Object());



// ... this doesn't!

Field<Object>  field4  = //...

Field<Object>  result4 = myFunction(field4, field4);

Field<Object>  result4 = myFunction(field4, (Field) field4);

Field<Object>  result4 = myFunction(field4, (Field<Object>) field4);

When <T> is bound to Object, all of a sudden both methods apply, and according to the JLS, none of them is more specific! While the Eclipse compiler is usually a bit more lenient (and in this case intuitively links the second method), the javac compiler doesn’t know what to do with this call. And there is no way around it. You cannot cast field4 to Field or to Field<Object> to force the linker to link to the second method. That’s pretty bad news for an API designer.

For more details about this special case, consider the following Stack Overflow question, which I reported as a bug to both Oracle and Eclipse. Let’s see which compiler implementation is correct:
http://stackoverflow.com/questions/5361513/reference-is-ambiguous-with-generics

The trouble with static imports, 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));

Reference: Overload API methods with care from our JCG partner Lukas Eder at the JAVA, SQL, AND JOOQ blog.

Related Articles :

Lukas Eder

Lukas is a Java and SQL enthusiast developer. He created the Data Geekery GmbH. He is the creator of jOOQ, a comprehensive SQL library for Java, and he is blogging mostly about these three topics: Java, SQL and jOOQ.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button