Monday, February 23, 2015

Sneaky exceptions

Streams and lambdas are powerfool constructs to facilitate functional style in Java 8. There is a specific inconvenience related to checked exception handling though. I'll illustrate the issue through a simple example: converting a collection of Strings - containing urls - to a collection of URL objects using the Stream API.
List<String> urlStrings = Arrays.asList(
    "http://google.com",
    "http://microsoft.com",
    "http://amazon.com"
);

List<URL> urls = urlStrings.stream().map(URL::new).collect(Collectors.toList());
This would look nice but it won't compile: the constructor of the URL throws a checked exception: MalformedURLException. The map method expects a Function, it's apply method implemented by the lambda expression doesn't declare any checked exceptions:
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
Placing the stream operation into a try block or declaring the exception in throws of the method containing it wouldn't help anything - the Function / lambda expression itself won't compile. The only viable option is to handle the exception within the Function. This sacrifices quite a bit of brevity:
List<URL> urls = urlStrings
    .stream()
    .map(url -> {
        try {
            return new URL(url);
        } catch (MalformedURLException e) {
            throw new SomeRuntimeException(e);
        }
    }).collect(Collectors.toList());
Compact code is one thing, the bigger issue is the original exception can't be directly propagated - to be handled outside the stream for instance. There is a pattern that may come handy in such situations: the sneaky throw. The intent is to throw the original checked exception as is, but hide it from the method's signature - effectively imitating characteristics of runtime exceptions. In our case this would be useful for defining Functions that may throw checked exceptions that propagate from the stream operation.

The first step is to define an alternative Function that defines checked exceptions.
public class Sneaky {
    @FunctionalInterface
    public static interface SneakyFunction<T, R> {
        R apply(T t) throws Exception;
    }
    ...
}
This allows us to write lambda expressions identical to normal Functions. It won't be useful in itself, the Stream.map() expects a Function implementation. We need a way to transform our SneakyFunction into a Function.
public class Sneaky {
    @FunctionalInterface
    public static interface SneakyFunction<T, R> {
        R apply(T t) throws Exception;
    }

    public static <T, R> Function<T, R> function(SneakyFunction<T, R> function) {
    ...
 }
}
Type erasure can be used to hide the checked exception from the method signature and make it behave just like a runtime exception.
public class Sneaky {
    @FunctionalInterface
    public static interface SneakyFunction<T, R> {
        R apply(T t) throws Exception;
    }

    public static <T, R> Function<T, R> function(SneakyFunction<T, R> function) {
        return o -> {
            try {
                return function.apply(o);
            } catch (Exception e) {
                Sneaky.<RuntimeException>sneakyException(e);
                return null;
            }
        };
    }

    @SuppressWarnings("unchecked")
    private static <T extends Throwable> T sneakyException(Throwable t) throws T {
        throw (T) t;
    }
}
Transforming SneakyFunction to Function allows checked exception throws in the transformation code:
List<URL> urls = urlStrings.stream().map(Sneaky.function(URL::new)).collect(Collectors.toList());
This code compiles and runs fine, will throw MalformedURLException when needed and it's possible to propagate the exception easily. There are simpler and "safer" solutions for this specific problem of course, that's not the point. The need for propagating checked exceptions from Streams is not uncommon, and this pattern can come handy despite it's controversies. The same pattern may be applied to other functional or pseudo-functional interfaces, like the Consumer. Sneaky throws are used in quite a few libraries, and there are some that support sneaky throws, like Project Lombok.

No comments:

Post a Comment