1. Introduction
The Functional Interfaces provided by the JDK are not prepared properly for the handling of checked exceptions. If you want to read more about the problem, check this article.
In this article, we’ll look at various ways to overcome such problems using the functional Java library Vavr.
To get more information about Vavr and how to set it up, check out this article.
2. Using CheckedFunction
Vavr provides functional Interfaces that have functions that throw checked exceptions. These functions are CheckedFunction0, CheckedFunction1 and so on till CheckedFunction8. The 0, 1, … 8 at the end of the function name indicates the number of input arguments for the function.
Let’s see an example:
static Integer readFromFile(Integer integer) throws IOException {
// logic to read from file which throws IOException
}
We can use the above method inside a lambda expression without handling the IOException:
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
CheckedFunction1<Integer, Integer> readFunction = i -> readFromFile(i);
integers.stream()
.map(readFunction.unchecked());
As you can see, without the standard try-catch or the wrapper methods, we can still call exception throwing methods inside a lambda expression.
We must exercise caution while using this feature with the Stream API, as an exception would immediately terminate the operation – abandoning the rest of the stream.
3. Using Helper Methods
The API class provides a shortcut method for the example in the previous section:
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.stream()
.map(API.unchecked(i -> readFromFile(i)));
4. Using Lifting
To handle an IOException gracefully, we can introduce standard try-catch blocks inside a lambda expression. However, the conciseness of a lambda expression will be lost. Vavr’s lifting comes to our rescue.
Lifting is a concept from functional programming. You can lift a partial function to a total function that returns an Option as result.
A partial function is a function that is defined only for a subset of a domain as opposed to a total function which is defined for the entirety of its domain. If the partial function is called with input that is outside of its supporting range, it will typically throw an exception.
Let’s rewrite the example from the previous section:
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.stream()
.map(CheckedFunction1.lift(i -> readFromFile(i)))
.map(k -> k.getOrElse(-1));
Note that the result of the lifted function is Option and the result will be Option.None in case of an exception. The method getOrElse() takes an alternate value to return in case of Option.None.
5. Using Try
While the method lift() in the previous section solves the issue of abrupt program termination, it actually swallows the exception. Consequently, the consumer of our method has no idea on what resulted in the default value. The alternative is to use a Try container.
Try is a special container with which we can enclose an operation that might possibly throw an exception. In this case, the resulting Try object represents a Failure and it wraps the exception.
Let’s look at the code that uses Try:
List<Integer> integers = Arrays.asList(3, 9, 7, 0, 10, 20);
integers.stream()
.map(CheckedFunction1.liftTry(i -> readFromFile(i)))
.flatMap(Value::toJavaStream)
.forEach(i -> processValidValue(i));
To learn more on the Try container and how to use it, check this article.
6. Conclusion
In this quick article, we showed how to use the features from the Vavr library to circumvent the problems while dealing with exceptions in lambda expressions.
Although these features allow us to elegantly deal with exceptions, they should be used with utmost care. With some of these approaches, consumers of your methods may be surprised with unexpected checked exceptions, although they are not explicitly declared.
The complete source code for all the examples in this article can be found over on Github.