• Tidak ada hasil yang ditemukan

Dealing with Exceptions

Dalam dokumen Book Functional Programming in Java (Halaman 98-102)

Java programmers are quite opinionated about checked exceptions. Irrespec- tive of how we feel about them, checked exceptions are here to stay and we

4. https://wiki.scala-lang.org/display/SYGN/Loan

Dealing with Exceptions

83

have to deal with them. Let’s look at some options for working with them in the context of lambda expressions.

In the next example we create a lambda expression that invokes a method that potentially throws a checked exception. We take a list of path names and ask for their canonical path using the getCanonicalPath() method.

public class HandleException {

public static void main(String[] args) throws IOException { Stream.of("/usr", "/tmp")

.map(path -> new File(path).getCanonicalPath()) .forEach(System.out::println);

//Error, this code will not compile }

}

We’ve decorated the main() method with the throws clause. However, when we compile this code the Java compiler will report an error:

... unreported exception IOException; must be caught or declared to be thrown .map(path -> new File(path).getCanonicalPath())

^ 1 error

The error is directly from within the lambda expression passed to the map() method. This method expects as a parameter an implementation of the Function interface. The apply() method of the Function interface does not specify any checked exceptions. So, our lambda expression that stands in for the abstract method in this example is not permitted to throw any checked exceptions.

We’re limited to two options here: we could either handle the exception right there within the lambda expression, or catch it and rethrow it as an unchecked exception. Let’s try the first option:

Stream.of("/usr", "/tmp") .map(path -> {

try {

return new File(path).getCanonicalPath();

} catch(IOException ex) { return ex.getMessage();

} })

.forEach(System.out::println);

In this scenario, if there were an exception, we’d return the exception details instead of the canonical path. Alternatively, to make it easier to identify suc- cess versus failure, we could return an object instead of a string. We could design the result object to carry two fields, one with a valid path response and the other with an optional error message.

Instead of handling the exception, we could replace the return within the catch with a throw new RuntimeException(ex); and propagate the exception—see Integrating with the Web Service, on page 71, for an example of this approach.

We can improve the client code’s readability quite a bit by creating static helpers for this catch and rethrow.

Transforming from a checked to an unchecked exception and rethrowing may work fine if the calls to the lambda expressions are made sequentially. If the calls are made concurrently from different threads, however, there are a few caveats.

In a concurrent execution, an exception raised within the lambda expressions will be propagated automatically to the calling primary thread. There are two snags: First, this will not terminate or obstruct the execution of other lambda expressions running concurrently. Second, if exceptions are thrown from multiple concurrent executions, only one of them will be reported in the catch block. If the details of all the exceptions are important, it’s better to capture those within the lambda expressions and pass them back to the main thread as part of the result.

In the previous example we were limited to the Function interface since the map() method relies on it. When we design our own higher-order functions based on our specific needs, we can more flexibly design the companion functional interfaces to go with it. For example, the next code shows a functional interface whose method specifies a checked exception using the throws clause.

resources/fpij/UseInstance.java

@FunctionalInterface

public interface UseInstance<T, X extends Throwable> { void accept(T instance) throws X;

}

Any method that accepts a parameter of the UseInstance interface will expect and be ready to handle appropriate exceptions or propagate them. We’ll take a closer look at this design option in Using Higher-Order Functions, on page 92.

The preceding idiom is tailored for lambda expressions that throw exactly one exception—for example, IOException. If the lambda expressions were to throw one of many exceptions, such as IOException or SQLException, then the parametric type X would need to be modeled as the least upper-bound of these exceptions

—namely, Exception. If we want to use this to model a lambda expression that throws no exception at all, we’d have to model the parametric type X as RuntimeException.

Dealing with Exceptions

85

Exception handling is a sticky issue; when designing and programming with lambda expressions we have to take extra care to handle them properly.

Recap

Lambda expressions are not just a language feature; they turn into a very powerful yet lightweight design tool. Instead of spending the effort to create a hierarchy of interfaces and classes, we can reuse functional interfaces and pass around lambda expressions and method references where possible. This technique can help us easily create delegates to quickly implement the strategy pattern at both the method and the class level. We can also use lambda expressions to implement the decorator pattern. By turning lambda expressions into controlled workhorses, we can create easy-to-read, fluent interfaces as well as configuration details in code. We must take extra care, however, to properly deal with exceptions when working with lambda expressions.

In the next chapter we’ll explore a variation of the loan pattern; we’ll use lambda expressions to exert greater control when managing object lifetime.

CHAPTER 5

No one is useless in this world who lightens the burden of it to anyone else.

Charles Dickens

Working with Resources

We may have been led to believe that the JVM automates all garbage collection (GC). It’s true that we could let the JVM handle it if we’re only using internal resources. However, GC is our responsibility if we use external resources, such as when we connect to databases, open files and sockets, or use native resources.

Java provides a few options to properly clean up resources, but, as we’ll see in this chapter, none are as effective as what we can do with lambda expres- sions. We’ll use lambda expressions to implement the execute around method (EAM) pattern, which gives us better control over sequencing of operations.1 Then we’ll use this pattern to do even more: manage locks and write exception tests.

Dalam dokumen Book Functional Programming in Java (Halaman 98-102)