Introduction
Have you ever come across a situation when working with Java Streams where you had to write a lambda expression that has to throw a checked exception? You wouldn’t have been able to use that lambda expression (or the equivalent method reference) throwing exception because the Functional Interfaces that Java streams have do not support throwing a checked exception. In this post, we will see 4 ways to throw checked exceptions in Java Streams. We will look at various options to throw an exception from a Java lambda. I also have an additional bonus approach as well.
Lambda expression in Streams and checked exceptions
We start with a list of file paths and read the content of each as a List<String> and collect them into another list.
List<String> paths = List.of("/home/file1", "/home/file2");
List<List<String>> fileContents = paths.stream()
.map(path -> Paths.get(path))
.map(path -> Files.readAllLines(path))
.collect(Collectors.toList());
In the above code, the function passed to the second map will fail to compile with the following error:
Unhandled exception: java.io.IOException
This is because of the method signature or contract of the Function functional interface. Let us look at how a Function is defined. A Function has a single abstract method as shown below:
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
}
It accepts an argument of type T and returns a value of type R. We can write the lambda expression we passed to the map method as an anonymous class:
Function<String, List<String>> readAllLines = new Function<String, List<String>>() {
@Override
public List<String> apply(String path) {
return Files.readAllLines(path);
}
};
Again, this doesn’t compile for the same reason.
The apply method does not support throwing a checked exception. But the call to Files.readAllLines can throw an IOException. Hence, the statement Files.readAllLines does not adhere to the Function’s apply() method contract.
Now let us see a few ways on how we can handle this. The first two methods do not use any external (3rd party) libraries whereas the last two do.
Approach 1 - Move the checked exception throwing method call to a separate function
The essence of this is to catch the checked exception and wrap it in a unchecked exception (See Difference between a checked and an unchecked exception).
The mechanics are as follows:
- Move the method call in the lambda expression that can throw a checked exception into a separate private method.
- Add a catch block to catch the checked exception(s).
- Wrap them in a runtime exception and throw it back.
List<List<String>> fileContents = paths.stream()
.map(path -> Paths.get(path))
.map(this::readFromFile)
.collect(Collectors.toList());
private List<String> readFromFile(Path path) {
try {
return Files.readAllLines(path);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
With the method reference this::readFromFile
, we now use the above defined private function to handle the thrown checked exception. The readFromFile method calls the readAllLines method and returns the result. It also catches the IOException and wraps it in a RuntimeException.
So, if the call to the readAllLines method throws an IOException, we will get it back wrapped in a RuntimeException.
Pros and Cons
This approach did not really handle the problem as it just moved it to a different place. In fact, this when written inline would look like:
List<List<String>> fileContents = paths.stream()
.map(path -> Paths.get(path))
.map(path -> {
try {
return Files.readAllLines(path);
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.collect(Collectors.toList());
The merit of this is that the method invocation and exception handling when moved to a private method would
- Keep the stream pipeline clean.
- Avoids long lambda expressions.
Approach 2 - Create a new corresponding Functional interface that can throw checked exceptions
Let us look at a different example. We have a method that takes a function and a parameter as arguments, applies the function with the given parameter and returns the result.
public static String process(Function<String, String> function, String param) {
String result = function.apply(param);
System.out.println(result);
return result;
}
When we invoke it as:
process(s -> s + "-1", "abcd");
Would print and return the string abcd-1
as the function appends -1 to the string passed in to the function.
However, if we pass a function that can throw a checked exception, this will not compile. Example: Let us pass a function that reads from a file.
process(path -> Files.readString(Paths.get(path)), "/home/file1");
Note that Files.readString method is available from Java 11+. But you can substitute it with any function that throws an exception.
We will get the same compilation error as before because of the same reason. The first parameter of the process method needs a Function, but the lambda expression used here can throw a checked exception. Thus, it does not satisfy the Function contract.
Define a new Functional interface with checked exception
We will create a new FunctionalInterface similar to that of a Function but that can throw an Exception. It is shown below.
@FunctionalInterface
public interface ThrowingFunction<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t) throws Exception;
}
Now, we can change the method parameter from accepting a Function to a ThrowingFunction.
public String processWithThrowingFunction(ThrowingFunction<String, String> function,
String param) throws Exception {
String result = function.apply(param);
System.out.println(result);
return result;
}
//Call it as
processWithThrowingFunction(path -> Files.readString(Paths.get(path)), "/home/file1");
The same lambda expression path -> Files.readString(Paths.get(path))
now satisfies the Functional Interface ThrowingFunction because the single abstract method in it (apply) can throw any exception. Thus we can pass the call to Files.readString as a ThrowingFunction.
The problem with this is that now the function processWithThrowingFunction has throws Exception in its method signature. This means that the caller to handle this. Or, as seen earlier, we have to catch the possible exception from invoking the ThrowingFunction inside the method.
Approach 3 - Creating a bridge from the throwing functional interface
The second approach requires changing a Function to a ThrowingFunction. Unfortunately, we cannot do it in a Java stream because the map method accepts only a Function.
We can create a bridge or conversion between these two types i.e., convert a ThrowingFunction into a Function using a simple utility method.
public static <T, R> Function<T, R> transform(ThrowingFunction<T, R> throwingFunction) {
return param -> {
try {
return throwingFunction.apply(param);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
};
}
The transform method takes a ThrowingFunction and returns a new Function (written as a lambda expression). The lambda expression parameter param is the Function interface’s argument to the apply method. When written as an anonymous class would look like:
return new Function<T, R>() {
@Override
public R apply(T t) {
try {
return throwingFunction.apply(t);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
};
Here’s how we would use this:
- Write a lambda expression (or method reference) that can throw a checked expression (which is of type ThrowingFunction).
- Call the transform method by passing it to get back a normal Function.
We can use now use this where a regular Function is used.
process(transform(path -> Files.readString(Paths.get(path))), "/home/file1");
The call to transform returns a Function. Thus we can even pass it to the process method. Any IOException would be thrown back, wrapped in a RuntimeException.
Using the bridge in a stream pipeline
Let us use the transform method to convert a ThrowingFunction into a Function and use it in the Stream’s map method.
List<List<String>> fileContents = paths.stream()
.map(path -> Paths.get(path))
.map(transform(Files::readAllLines)) //transform(path -> Files.readAllLines(path))
.collect(Collectors.toList());
When we call transform, it returns a new function that invokes readAllLines, but catches any checked exception and throws back as a RuntimeException.
Advantage of creating new functional interface
So far, though, we have been seeing from the standpoint of a Function. But we can apply this to all functional interfaces in Java like Supplier, Predicate, Consumer, etc.
Once we create one checked version of Functional Interface for each of the Functional Interfaces and create a bridge function to transform it, we can use it from many places.
Approach 4 - Streams utility from Apache Commons Lang
This approach uses an utility method from the Apache Commons Lang library to throw checked exceptions in Java Streams. Hence, you need to have a dependency on this library for this.
The Apache Commons Lang has an utility class called Streams which provides functions for working with Java streams. It helps us to convert a Java Stream into a FailableStream which supports all operations on a Stream. In addition to that, in a FailableStream, the map, filter methods (and others) work with Failable version of the Functional Interfaces like FailablePredicate, FailableConsumer, and FailableFunction etc.,
These Failable* interfaces are defined within the Apache Commons Lang library. I have shown a few of them below:
public FailableStream<O> filter(final FailablePredicate<O, ?> predicate)
public <R> FailableStream<R> map(final FailableFunction<O, R, ?> mapper)
public void forEach(final FailableConsumer<O, ?> action)
These functional interfaces are declared to throw exception (very similar to the ThrowingFunction we created). For example, FailableFunction is defined as
@FunctionalInterface
public interface FailableFunction<I, O, T extends Throwable> {
/**
* Apply the function.
* @param input the input for the function
* @return the result of the function
* @throws T if the function fails
*/
O apply(I input) throws T;
}
where the exception it throws is a type parameter (T). When it is used in the stream, they have used a wildcard (?). Hence, we can throw any checked exception.
Apache Commons Lang - Streams#stream method
There are two Streams.stream methods which accept
- A Java stream or
- A Java Collection
and return a FailableStream. We can pass lambda expressions that can throw a (checked) exception to the map, filter, forEach and other operations.
List<String> paths = List.of("/home/file1", "/home/file2");
List<List<String>> fileContents = Streams.stream(paths.stream())
.map(path -> Paths.get(path))
.map(Files::readAllLines) //path -> Files.readAllLines(path)
.collect(Collectors.toList());
fileContents = Streams.stream(paths)
.map(path -> Paths.get(path))
.map(Files::readAllLines) //path -> Files.readAllLines(path)
.collect(Collectors.toList());
BONUS approach - Lombok’s SneakyThrows
The last approach uses the Lombok library. This approach is very hacky and not recommended and requires careful usage.
It provides an annotation called @SneakyThrows. It is used to sneakily throw checked exception without declaring throws clause in the method’s signature. You can think of this as cheating the compiler. The documentation states:
The code generated by lombok will not ignore, wrap, replace, or otherwise modify the thrown checked exception; it simply fakes out the compiler. On the JVM (class file) level, all exceptions, checked or not, can be thrown regardless of the
throws
clause of your methods, which is why this works.
We can pass the list of exception classes to the @SneakyThrows annotation that we wish to be thrown.
@SneakyThrows(IOException.class)
private List<String> readFromFile(Path path) {
return Files.readAllLines(path);
}
This method calls Files.readAllLines which can throw an IOException. But we got away with neither catching the checked exception nor declaring it in the method signature in the throws clause.
List<String> paths = List.of("/home/file1", "/home/file2");
List<List<String>> fileContents = paths.stream()
.map(path -> Paths.get(path))
.map(this::readFromFile)
.collect(Collectors.toList());
The method reference used in the map calls the above method to read from the file. And we wrote the method without having to handle the checked exception (very sneaky). We could still get an IOException during runtime.
Conclusion
This post, 4 ways to throw checked exceptions in Java streams, started by looking at the problem of having a lambda expression that can throw checked exception(s) when using it within Java streams. We looked at four ways to handle them where the last two methods used external libraries.
It is up to you to choose which method to use (except that I do not suggest the Lombok approach unless you know what you are doing and have a good reason to do so).
References
- https://stackoverflow.com/questions/18198176/java-8-lambda-function-that-throws-exception
- Apache Commons lang Streams
- Lombok SneakyThrows
- Exception in lambdas - Nicolas has an interesting approach using Vavr library