
Java Exception and Lambda Part 2 : Give power back to the compiler
In Part 1 of this series we talked about how lamba expressions introduced since Java 8 are mapping to functional interfaces and how the absence of any throws
clause in the standard functional interfaces provided in the JDK somewhat opened the door to the idea of exploiting the sneaky throw technique. We also found how dangerous that compiler “feature” can be when misused and how easy it is to misuse it. We left with some hope that there might be a way to refactor our code in a way that would allow the java compiler to still be able to perform checked exception validation while still being able to leverage the sneaky throw technique for lamba expressions, so let’s think about this a little more…
First we will adapt a bit our legacy code example from Part 1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
try { List<Customer> customers = queryDatabaseForCustomers(); // Expected to throw SQLException List<Order> orders = handleAllOrdersOf(customers); // Expected to throw SQLException // process orders } catch (SQLException e) { // Handle the exception } private List<Order> handleAllOrdersOf(List<Customer> customers) throws SQLException { return Optional.ofNullable(customers) .map(unchecked(this::queryDatabaseForAllOrders)) // Now using sneaky throw technique .orElseGet(Collections::emptyList); } public static <T, R> Function<T, R> unchecked(CheckedFunction<T, R> checkedFunction) { return t -> { try { return checkedFunction.apply(t); } catch (Exception e) { return sneakyThrow(e); } }; } public static <T, E extends Exception> T sneakyThrow(Exception e) throws E { throw (E) e; } |
So assuming our unchecked()
method is now implemented to use the sneaky throw technique, from the above we can see that the obvious way to guarantee type safety and a consistent behavior for exception handling is to add throws SQLException
to the method signature of handleAllOrdersOf()
i.e. make sure our outer public method (our api) declares the same checked exception as the queryDatabaseForAllOrders()
method used in our implementation as a method reference passed to our unchecked()
. So we sneaky throw an SQLException
from our implementation of queryDatabaseForAllOrders()
but we also let the compiler know that handleAllOrdersOf()
can throw an SQLException
, so now we have type safety back… or do we?
It’s working for now but…
What if a few months later in another refactoring effort, someone change the method signature of queryDatabaseForCustomers()
to now throw e.g. an IOException
or another custom checked exception type? This is something we all did, change a method signature and let the compiler spit out compilation errors as a way to find all places where the method is used, then just replace the catch
block to handle the new exception type. It is going to work… everywhere except for our code, the compiler will still not catch the exception type mismatch between our method implementation that sneaky throw SQLException
(as a RuntimeException
) and our method declaration that now throws IOException
. So how can we statically link the relation between the checked exception type thrown by our outer method and the lambda expression we wrap in unchecked()
?
First we need our functional interface CheckedFunction
to declare a specific exception type instead of a generic Exception
:
1 2 3 4 |
@FunctionalInterface public interface CheckedFunction<T, R, E extends Exception> { R apply(T t) throws E; } |
So before this change our method reference was mapped as:
1 |
CheckedFunction<List<Customer>, List<Order>> f = this::queryDatabaseForAllOrders; |
i.e. the apply()
method was resolving to:
1 |
List<Order> apply(List<Customer> t) throws Exception |
So we were losing the exception type information for SQLException
, but by changing the definition of our CheckedFunction
as we did above, our method reference now maps to:
1 |
CheckedFunction<List<Customer>, List<Order>, SQLException> f = this::queryDatabaseForAllOrders; |
and the apply()
now resolves to:
1 |
List<Order> apply(List<Customer> t) throws SQLException |
Consequently the definition of our unchecked()
method now becomes:
1 2 3 4 5 6 7 8 9 10 |
public static <T, R, E extends Exception> Function<T, R> unchecked( CheckedFunction<T, R, E> checkedFunction) { return t -> { try { return checkedFunction.apply(t); } catch (Exception e) { return sneakyThrow(e); } }; } |
The next step involves refactoring our logic for handleAllOrdersOf()
by creating a more generic method that will take as a parameter our new CheckedFunction
interface and will declare a throws
clause of the same exception type as the CheckedFunction
we are passing:
1 2 3 4 5 6 |
private <T, R, E extends Exception> List<R> processSubQuery(List<T> topLevelEntities, CheckedFunction<List<T>, List<R>, E> subQueryFunction) throws E { return Optional.ofNullable(topLevelEntities) .map(unchecked(subQueryFunction)) .orElseGet(Collections::emptyList); } |
Notice the type E
at line 2? The exception type of the lamba expression passed as a higher order function parameter is now statically linked to the throws
clause of our public method declaration. We can now invoke our new method this way:
1 2 3 4 5 6 7 8 9 |
public void processCustomers() { try { List<Customer> customers = queryDatabaseForCustomers(); List<Order> orders = processSubQuery(customers, this::queryDatabaseForAllOrders); // Process orders } catch (SQLException e) { // Handle the exception } } |
We killed two birds with one stone here, we explored a pattern to create reusable functions (e.g. processSubQuery
) that can be used in other contexts (our specific example here is not much useful outside the context of this blog post, but the focus should be about the general idea of passing higher order function as parameter to avoid hardcoding logic in our method implementation), but more importantly we regain the type safety and exception checking at compile time that we lost, if we change the definition of the method queryDatabaseForAllOrders
to throw e.g. an IOException
, our code above will fail to compile with a “Unhandled exception: java.io.IOExeception” error.
Some caveats
One “limitation” of this design is that queryDatabaseForAllOrders()
cannot throw more than one checked exception (SQLException
in this example), the word “limitation” being put in quotes here because although this is still an ongoing debate in the java community regarding if checked exceptions are a mistake (vs. only using runtime exceptions, strategy taken by other languages e.g. Kotlin, Scala or C#), the community seems to lean a bit more toward the fact that throwing more than one checked exception from a public method can be seen as an anti-pattern and a code smell, although this is also not a unanimous consensus. So this could be another subject for a subsequent blog post, but let’s assume for now that we stick with the at most one checked exception per public method rule.
Another very important point to consider is, assuming you are in the pro checked exceptions camp and want the java compiler to perform exception type checks instead of going all in with runtime exceptions (as we would do in other languages like Kotlin or Scala, or as with some java libraries like Spring Data), when designing a solution like the above, make sure that the lamba expression wrapped with a function that hides the checked exception and use the sneaky throw technique (e.g. unchecked()
in the example above) is actually invoked within the scope of our method using it (e.g. processSubQuery()
). In our case this assumption is valid, but beware of lazy evaluation code, for example what if our lambda expression is used as part of e.g. constructing a Stream
that is returned by our api and then consumed later outside the scope of our method that wrapped our lamba expression with unchecked()
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
private <T, R, E extends Exception> Stream<R> createStream(List<T> list, CheckedFunction<T, R, E> function) throws E { return list.stream() .map(unchecked(function)); // function not invoked here } private String convert(Integer i) throws IOException { throw new IOException(); } Stream<String> stream = null; try { stream = createStream(asList(1, 2), this::convert); } catch (IOException e) { // We will never get here } List<String> list = stream .collect(toList()); // convert() invoked here |
Here the execution of our convert()
function escaped the scope of createStream() and will be triggered outside the try/catch
block above, so an IOException
will be sneaky thrown as a runtime exception i.e. we will still have the problem we tried to resolved with the blog post you are currently reading ;-).
Takeway
With a bit of refactoring we were able to give back some power to the compiler for exception checking even when using the sneaky throw technique to overcome the lack of support for checked exceptions with Java 8 lamba expressions, assuming the execution of the lamba expression doesn’t escape the scope of the code referencing it, which should be the case most of the time. But as you can see this is still not 100% bulletproof i.e. as a developer we still need to think about the workflow of our program to avoid the edge case described above.
Stay tuned for part 3 of this series where we look into how functional programming principles would help make the whole exception handling with lamba expressions much more fluent without breaking the flow of our program, and then try to answer the question as to if the sneaky throw technique would still be relevant.