Java lambdas and exception handling
Following on from our introduction to lambdas in Java, we turn to the issue of throwing exceptions from lambda expressions.
Recall that Java exceptions can be checked or unchecked. Checked exceptions are declared in the throws clause of a
method. The Java compiler forces such exceptions to be explicitly caught by the caller of that method. Unchecked exceptions, on the other hand, need not be
declared and caught: if uncaught, they will simply be "passed up the chain" until they are eventually handled, either by
a caller further up the call stack or by the uncaught exception handler. Common examples of unchecked exceptions are
NullPointerException and IllegalArgumentException; other examples are any subclass of RuntimeException.
The rules for when you can throw an exception from a lambda are effectively the same as for when you can throw
an exception from a method:
- you can always throw an unchecked exception such as NullPointerException
from a lambda expression;
- you can throw a checked exception only only if the functional interface
declares that exception;
As we will see, the general-purpose interfaces in the java.util.function (such as Function, Consumer etc)
generally do not allow checked exceptions to be thrown. The practical consequence of this that the with the
Java Stream API, workarounds are required when checked exceptions are involved.
Example where we can throw a checked exception from a lambda expression: Callable
The Callable interface, introduced in Java 5, is similar to Runnable. It declares a single call()
method, which is declared as throwing Exception, allowing any checked exception to be thrown:
public interface Callable<V> {
V call() throws Exception;
}
Therefore, if we define a Callable using a lambda expression, that lambda expression can throw a checked exception
(or call a method that throws a checked exception). This example calls Files.readAllLines(), which can throw IOException:
Callable<String[]> configFetcher = () -> Files.readAllLines(configPath);
In this example, the lambda code explicitly throws a TimeoutException:
Callable<Integer> threadsafeCalc = () -> {
if (!lock.tryLock(TIMEOUT, TimeUnit.MILLISECONDS)) {
throw new TimeoutException("Could not acquire lock in time");
}
try {
return calculateValue0();
} finally {
lock.unlock();
}
};
Examples where the lamda expression cannot throw a checked exception: Function and Consumer
Now we turn to the case where a checked exception cannot be thrown from within a lambda expression. The Function
interface, like most of the general-purpose functional interfaces in the java.util.function package,
has a method which is not declared as throwing any exceptions:
public interface Function<T, R> {
R apply(T t);
}
The following is therefore not possible:
// Fails to compile:
Function<Path, String[]> lineReader = (path) -> Files.readAllLines(path);
For similar reasons, the following is not possible:
List<Path> pathsToDelete = ....
// Fails to compile:
pathsToDelete.forEach(Files::deleteIfExists);
Here, forEach takes a lConsumer. And the method Consumer.accept(),
is not declared as throwing any exceptions. Therefore, we cannot simply call Files.deleteIfExists(),
which throws a checked exception (IOException).
We therefore require a workaround for these cases when we need to throw an exception from a lambda expression,
but the corresponding functional interface does not allow us to do so. One way or another, we must explicitly
catch and deal with the exception so that no checked exception is thrown from the lambda exprssion itself. We essentially
have two choices:
- we can "re-cast" the checked exception as an unchecked exception (either a RuntimeException
or a more specific subclass);
- we can catch the exception, but instead of throwing it, register that the error has occurred in some other way.
To "re-cast" the exception, we catch it and then throw it wrapped in a RuntimeException:
pathsToDelete.forEach(path -> {
try {
Files.deleteIfExists(path);
} catch (IOException ioex) {
throw new RuntimeException("Unexpected I/O error while deleting " + f, ioex);
}
})
Here we simply use a generic RuntimeException. Because IOException is a common type of exception,
a corresponding UnchckedIOException class also exists, and we could use that instead if it was important to
distinguish between the error being an IOExeption vs some other type of exception. We could also define
our own exception class: so long as it subclasses RuntimeException, we can throw it from a lambda
expression and it will still be compatible with the Stream API.
If you have many cases like this where you need to handle checked exceptions inside lambdas, it may be worth
creating a utility method to provide a little syntax sugar. For example, by declaring the following once:
private interface ConsumerWithError<T> {
void accept(T val) throws Exception;
}
public static <T> Consumer<T> withError(ConsumerWithError<T> cons) {
return (val) -> {
try {
cons.accept(val);
} catch (Exception e) {
throw new RuntimeException("Unexpected exception", e);
}
};
}
we can then write:
paths.forEach(withError(Files::deleteIfExists));
Note: an exception halts the Stream pipeline
If we do opt for the latter approach, then we need to bear in mind that if the exception is thrown, it will halt
stream processing: in other words, a failure to delete one file in this case will stop subsequent files from
being deleted. This is also the case with any other unchecked exception that might be thrown from a labmda expression
within the steam, whether explicitly or not: these include NullPointerException, IllegalArgumentException,
NumberFormatException...
If you enjoy this Java programming article, please share with friends and colleagues. Follow the author on Twitter for the latest news and rants.
Editorial page content written by Neil Coffey. Copyright © Javamex UK 2021. All rights reserved.