Handling checked exceptions in Java streams

Know your options for managing checked exceptions in Java 8’s functional approach.

By Ken Kousen
September 20, 2017
Train at night Train at night (source: biacamentil)

Several decisions were made during the creation of the Java language that still impact how we write code today. One of them was the addition of checked exceptions to the language, which the compiler requires you to prepare for with either a try/catch block or a throws clause at compile time.

If you’ve moved to Java 8, you know that the shift to functional programming concepts like lambda expressions, method references, and streams feels like they completely remade the language. Rather than iterate over a collection of values, we transform streams using a pipeline. Instead of “if” conditions, we use predicates in a filter. Instead of describing in detail how to accomplish a task, we take a more declarative approach and specify what we want to the code to do and take advantage of a greatly enhanced standard library.

Learn faster. Dig deeper. See farther.

Join the O'Reilly online learning platform. Get a free trial today and find answers on the fly, or master something new and useful.

Learn more

Developers moving to Java 8 adopt the new approach pretty easily, and the resulting code tends to be shorter and easier to follow, unless you have to deal with exceptions. Here you’ll see three primary ways to handle exceptions in a stream pipeline and each approach will include several examples.

Unchecked Exceptions

Imagine you have a collection of integers, and you want to divide each of them by a constant. A scaling operation like that can be expressed using a stream as in Example 1.

Example 1. A lambda expression that may throw an unchecked exception

public List<Integer> scale(List<Integer> values, Integer factor) {
    return values.stream()
        .map(n -> n / factor)
        .collect(Collectors.toList());
}

The scale method takes a List of integers and a factor to scale by. The stream() method (a new default method added to the Collection interface) converts the collection into a Stream<Integer>. The map method takes a Function, one of the new functional interfaces in the java.util.function package, which takes each value n and divides it by the factor. Then the collect method is used to convert the results back into a List.

The function argument to the map method will encounter a problem if the supplied factor is zero. In that case the integer division will throw an ArithmeticException.

(Aside: as an experiment, try dividing a number by the double value 0.0 instead of the integer value 0. You may be either amused or horrified to find that no exception is thrown. In that case, the returned value is Infinity (seriously), and any subsequent calculations will continue on, corrupting values along the way. Believe it or not, this is considered correct behavior according to the IEEE 754 specification for handling floating point calculations in a binary computer.)

Since ArithmeticException is an unchecked exception, you are not required to prepare for it by adding either a try/catch block or a throws clause to the code. In other words, this code is fine as it is, at least as far as the compiler is concerned.

What would you do if you did want to deal with the thrown exception?

Using a try/catch block vs. an extracted method

The easiest answer is to embed a try/catch block inside the pipeline, as shown in Example 2.

Example 2. Lambda expression with try/catch

public List<Integer> scale(List<Integer> values, Integer factor) {
    return values.stream()
        .map( n -> {
            try {
                return n / factor;
            } catch (ArithmeticException e) {
                e.printStackTrace();
            }
        })
        .collect(Collectors.toList());
}

This works, but makes the code harder to read and understand. Ideally when writing pipeline code, you would like to keep each intermediate operation as a single line. While that may not always be practical, it’s a good goal that makes the result easier to follow and debug if necessary.

A good alternative is to extract the function argument to map into a method of its own, as in Example 3.

Example 3. Extracting a lambda into a method

private Integer divide(Integer value, Integer factor) {
    try {
        return value / factor;
    } catch (ArithmeticException e) {
        e.printStackTrace();
    }
}

public List<Integer> scale(List<Integer> values, Integer factor) {
    return values.stream()
        .map(n -> divide(n, factor))
        .collect(Collectors.toList());
}

Developers have been writing code like the divide method for years. It looks familiar, is easy to understand, and (even better) you can write test cases to make sure it is working the way you expect. Best of all, the scale method is now simplified so that the intermediate map operation is only a single line.

All this effort wasn’t really necessary here, however, because ArithmeticException is unchecked. The same process works, however, for checked exceptions.

Dealing with checked exceptions

The compiler requires you to prepare for checked exceptions, so simply ignoring the problem is no longer an option. Instead, you have three primary approaches:

  1. Add a try/catch block to the lambda expression
  2. Create an extracted method, as in the unchecked example
  3. Write a wrapper method that catches checked exceptions and rethrows them as unchecked
Note

A complete example, including tests for all three encoding approaches and a demonstration of several geocoded addresses, can be found in the GitHub repository at https://github.com/kousen/java-geocoder .

To give a concrete example, consider determining the latitude and longitude of an address using a public geocoder, like the one provided by Google as a RESTful web service. A geocoder takes address information, like street, city, and state, and converts it into geographical coordinates like latitude and longitude. To use the Google geocoder, you need to encode the components of a given address, transmit it to Google using the proper URL, and extract the results from the JSON or XML response.

Java already includes a java.net.URLEncoder class. That class includes a static encode method that converts strings into their URL form-encoded equivalents, in that spaces become pluses, apostrophes become %27s, and so on (see the Javadocs for URLEncoder for details). The problem is, the encode method declares that it throws an UnsupportedEncodingException, which is a checked exception.

Given strings representing an address, the encodeAddress method in Example 4 shows how you could encode each of them.

Example 4. URL encoding a collection of strings (NOTE: DOES NOT COMPILE)

public String encodeAddress(String... values) {
    return Arrays.stream(values)
        .map(s -> URLEncoder.encode(s, "UTF-8")))
        .collect(Collectors.joining(","));
}

The String varargs argument is treated as an array inside the method, so the Arrays.stream() method produces a Stream<String> from those values. Then, as before, a map operation with a function encodes each value, and the results are collected back into a String. Unfortunately, the code in Example 4 does not compile, because the checked UnsupportedEncodingException isn’t handled at compile time.

What you might be tempted to do is to simply declare that the encodeAddress method throws the proper exception, as in Example 5.

Example 5. Declaring the exception (ALSO DOES NOT COMPILE)

public String encodeAddress(String... values) throws UnsupportedEncodingException {
        return Arrays.stream(values)
            .map(s -> URLEncoder.encode(s, "UTF-8")))
            .collect(Collectors.joining(","));
}

This, too, does not compile, because the encodeAddress method isn’t the one that needs the throws clause. To see that, consider implementing the function argument to map as an anonymous inner class rather than a lambda, as shown in Example 6.

Example 6. URL encoding using try/catch — anonymous inner class version

public String encodeAddressAnonInnerClass(String... values) {
    return Arrays.stream(values)
        .map(new Function<String, String>() {
            @Override
            public String apply(String s) {
                try {
                    return URLEncoder.encode(s, "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                    return "";
                }
            }
        })
        .collect(Collectors.joining(","));
}

As this example shows, the place to put the throws clause would be on the apply method, and in a lambda version, you don’t even see the signature of that method. Even more, the Function interface (which declares the apply method) is from the library, and you can’t simply modify the methods declared in library interfaces. So you’re back to embedding a try/catch block inside the apply method again, as in Example 7.

Example 7. URL encoding using try/catch — lambda version

public String encodedAddressUsingTryCatch(String... address) {
    return Arrays.stream(address)
                .map(s -> {
                    try {
                        return URLEncoder.encode(s, "UTF-8");
                    } catch (UnsupportedEncodingException e) {
                        throw new RuntimeException(e);
                    }
                })
                .collect(Collectors.joining(","));
}

Using the extracted method approach

Instead of embedding the try/catch in a lambda, use the extracted method approach described with the scale example from above instead, as shown in Example 8.

Example 8. URL encoding delegating to an extracted method

private String encodeString(String s) {
    try {
        return URLEncoder.encode(s, "UTF-8");
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }
    return s;
}

public String encodedAddressUsingExtractedMethod(String... address) {
    return Arrays.stream(address)
            .map(this::encodeString)
            .collect(Collectors.joining(","));
}

Here the extracted method is called encodeString, and it contains the needed try/catch block. This works, and the pipeline code is even simpler because it uses a method reference to point to the encodeString method. In this case, the extracted method catches the checked exception and rethrows it as unchecked.

This approach isn’t bad, and in fact is pretty common, but it does require you to write the extracted method each time. Isn’t there a way to generalize that?

Generalizing the extracted method

The generalized way to handle all checked exceptions isn’t perfect, but the technique is worth knowing. The problem here is that the map method takes a Function, and the apply method in Function does not declare any exceptions. What if you created another functional interface, similar to Function, whose apply method did declare that it throws an exception? See Example 9 for such an interface.

Example 9. A functional interface based on Function that throws Exception

@FunctionalInterface
public interface FunctionWithException<T, R, E extends Exception> {

    R apply(T t) throws E;
}

The generic parameters T and R in FunctionWithException are just like their counterparts in Function, but the added E parameter extends Exception. The apply method now takes a T, returns an R, and declares it may throw an E.

Now you can write a method, here called wrapper, that takes an argument of type FunctionWithException and returns a Function, as in Example 10.

Example 10. A wrapper method to deal with exceptions

private <T, R, E extends Exception>
    Function<T, R> wrapper(FunctionWithException<T, R, E> fe) {
        return arg -> {
            try {
                return fe.apply(arg);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        };
}

The argument to the wrapper method is any FunctionWithException. The implementation embeds a try/catch block that catches any exception and rethrows it as an unchecked exception. The return type is a java.util.function. Function, which is the required argument for the map method. Example 11 demonstrates how to use the wrapper method.

Example 11. Using a generic wrapper method

public String encodedAddressUsingWrapper(String... address) {
    return Arrays.stream(address)
            .map(wrapper(s -> URLEncoder.encode(s, "UTF-8")))
            .collect(Collectors.joining(","));
 }

In this case, the lambda expression is invoking the URLEncode.encode method with a string argument and returns the encoded string. Of course, it may throw an UnsupportedEncodingException. The wrapper method catches that (or any other) exception and rethrows it as an unchecked exception, ultimately returning the instance of Function that the map method wants.

Complications and some Java community help

The downside to this approach is that in addition to FunctionWithException, you’re probably also going to need functional interfaces for consumers with exceptions, suppliers with exceptions, and predicates with exceptions, not to mention the primitive type and binary variations. That sounds like a fair amount of work, so is probably best left to library developers. Of course, that’s exactly what’s happening in the community now. The VAVR library, as an example, has methods similar to the ones just described.

It’s complications like this that make it clear why most popular Java frameworks (like Spring and Hibernate, to list just a couple of examples) catch all checked exceptions and rethrow them as unchecked.

All of these approaches are still in response to the decision made at the beginning of Java, which was to support checked exceptions as well as unchecked. That feature of Java is not likely to change any time soon, so it’s nice to know there are a few standard mechanisms you can use to handle them.

Post topics: Software Engineering
Share:

Get the O’Reilly Radar Trends to Watch newsletter