Java 8 Friday: API Designers, be Careful
At Data Geekery, we love Java. And as we’re really into jOOQ’s fluent API and query DSL, we’re absolutely thrilled about what Java 8 will bring to our ecosystem.
Java 8 Friday
Every Friday, we’re showing you a couple of nice new tutorial-style Java 8 features, which take advantage of lambda expressions, extension methods, and other great stuff. You’ll find the source code on GitHub.
Lean Functional API Design
With Java 8, API design has gotten a whole lot more interesting, but also a bit harder. As a successful API designer, it will no longer suffice to think about all sorts of object-oriented aspects of your API, you will now also need to consider functional aspects of it. In other words, instead of simply providing methods like:
void performAction(Parameter parameter); // Call the above: object.performAction(new Parameter(...));
… you should now think about whether your method arguments are better modelled as functions for lazy evaluation:
// Keep the existing method for convenience // and for backwards compatibility void performAction(Parameter parameter); // Overload the existing method with the new // functional one: void performAction(Supplier<Parameter> parameter); // Call the above: object.performAction(() -> new Parameter(...));
This is great. Your API can be Java-8 ready even before you’re actually targeting Java 8. But if you’re going this way, there are a couple of things to consider.
JDK dependency
The above example makes use of the JDK 8 Supplier
type. This type is not available before the JDK 8, so if you’re using it, you’re going to limit your APIs use to the JDK 8. If you want to continue supporting older Java versions, you’ll have to roll your own supplier, or maybe use Callable
, which has been available since Java 5:
// Overload the existing method with the new // functional one: void performAction(Callable<Parameter> parameter); // Call the above: object.performAction(() -> new Parameter(...));
One advantage of using Callable
is the fact that your lambda expressions (or “classic” Callable
implementations, or nested / inner classes) are allowed to throw checked exceptions. We’ve blogged about another possibility to circumvent this limitation, here.
Overloading
While it is (probably) perfectly fine to overload these two methods
void performAction(Parameter parameter); void performAction(Supplier<Parameter> parameter);
… you should stay wary when overloading “more similar” methods, like these ones:
void performAction(Supplier<Parameter> parameter); void performAction(Callable<Parameter> parameter);
If you produce the above API, your API’s client code will not be able to make use of lambda expressions, as there is no way of disambiguating a lambda that is a Supplier
from a lambda that is a Callable
. We’ve also mentioned this in a previous blog post.
“void-compatible” vs “value-compatible”
I’ve recently (re-)discovered this interesting early JDK 8 compiler bug, where the compiler wasn’t able to disambiguate the following:
void run(Consumer<Integer> consumer); void run(Function<Integer, Integer> function); // Remember, the above types are roughly: interface Consumer<T> { void accept(T t); // ^^^^ void-compatible } interface Function<T, R> { R apply(T t); // ^ value-compatible }
The terms “void-compatible” and “value-compatible” are defined in the JLS §15.27.2 for lambda expressions. According to the JLS, the following two calls are not ambiguous:
// Only run(Consumer) is applicable run(i -> {}); // Only run(Function) is applicable run(i -> 1);
In other words, it is safe to overload a method to take two “similar” argument types, such as Consumer
and Function
, as lambda expressions used to express method arguments will not be ambiguous.
This is quite useful, because having an optional return value is very elegant when you’re using lambda expressions. Consider the upcoming jOOQ 3.4 transaction API, which is roughly summarised as such:
// This uses a "void-compatible" lambda ctx.transaction(c -> { DSL.using(c).insertInto(...).execute(); DSL.using(c).update(...).execute(); }); // This uses a "value-compatible" lambda Integer result = ctx.transaction(c -> { DSL.using(c).update(...).execute(); DSL.using(c).delete(...).execute(); return 42; });
In the above example, the first call resolves to TransactionalRunnable
whereas the second call resolves to TransactionalCallable
whose API are like these:
interface TransactionalRunnable { void run(Configuration c) throws Exception; } interface TransactionalCallable<T> { T run(Configuration c) throws Exception; }
Note, though, that as of JDK 1.8.0_05 and Eclipse Kepler (with the Java 8 support patch), this ambiguity resolution does not yet work because of these bugs:
So, in order to stay on the safe side, maybe you could just simply avoid overloading.
Generic methods are not SAMs
Do note that “SAM” interfaces that contain a single abstract generic method are NOT SAMs in the sense for them to be eligible as lambda expression targets. The following type will never form any lambda expression:
interface NotASAM { <T> void run(T t); }
This is specified in the JLS §15.27.3
A lambda expression is congruent with a function type if all of the following are true:
- The function type has no type parameters.
- [ … ]
What do you have to do now?
If you’re an API designer, you should now start writing unit tests / integration tests also in Java 8. Why? For the simple reason that if you don’t you’ll get your API wrong in subtle ways for those users that are actually using it with Java 8. These things are extremely subtle. Getting them right takes a bit of practice and a lot of regression tests. Do you think you’d like to overload a method? Be sure you don’t break client API that is calling the original method with a lambda.
Reference: | Java 8 Friday: API Designers, be Careful from our JCG partner Lukas Eder at the JAVA, SQL, AND JOOQ blog. |