How can I throw CHECKED exceptions from inside Java 8 streams?

MarcG picture MarcG · Dec 25, 2014 · Viewed 118.3k times · Source

How can I throw CHECKED exceptions from inside Java 8 streams/lambdas?

In other words, I want to make code like this compile:

public List<Class> getClasses() throws ClassNotFoundException {     

    List<Class> classes = 
        Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
              .map(className -> Class.forName(className))
              .collect(Collectors.toList());                  
    return classes;
    }

This code does not compile, since the Class.forName() method above throws ClassNotFoundException, which is checked.

Please note I do NOT want to wrap the checked exception inside a runtime exception and throw the wrapped unchecked exception instead. I want to throw the checked exception itself, and without adding ugly try/catches to the stream.

Answer

Christian Hujer picture Christian Hujer · Dec 27, 2014

The simple answer to your question is: You can't, at least not directly. And it's not your fault. Oracle messed it up. They cling on the concept of checked exceptions, but inconsistently forgot to take care of checked exceptions when designing the functional interfaces, streams, lambda etc. That's all grist to the mill of experts like Robert C. Martin who call checked exceptions a failed experiment.

In my opinion, this is a huge bug in the API and a minor bug in the language specification.

The bug in the API is that it provides no facility for forwarding checked exceptions where this actually would make an awful lot of sense for functional programming. As I will demonstrate below, such a facility would've been easily possible.

The bug in the language specification is that it does not allow a type parameter to infer a list of types instead of a single type as long as the type parameter is only used in situations where a list of types is permissable (throws clause).

Our expectation as Java programmers is that the following code should compile:

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

public class CheckedStream {
    // List variant to demonstrate what we actually had before refactoring.
    public List<Class> getClasses(final List<String> names) throws ClassNotFoundException {
        final List<Class> classes = new ArrayList<>();
        for (final String name : names)
            classes.add(Class.forName(name));
        return classes;
    }

    // The Stream function which we want to compile.
    public Stream<Class> getClasses(final Stream<String> names) throws ClassNotFoundException {
        return names.map(Class::forName);
    }
}

However, it gives:

cher@armor1:~/playground/Java/checkedStream$ javac CheckedStream.java 
CheckedStream.java:13: error: incompatible thrown types ClassNotFoundException in method reference
        return names.map(Class::forName);
                         ^
1 error

The way in which the functional interfaces are defined currently prevents the Compiler from forwarding the exception - there is no declaration which would tell Stream.map() that if Function.apply() throws E, Stream.map() throws E as well.

What's missing is a declaration of a type parameter for passing through checked exceptions. The following code shows how such a pass-through type parameter actually could have been declared with the current syntax. Except for the special case in the marked line, which is a limit discussed below, this code compiles and behaves as expected.

import java.io.IOException;
interface Function<T, R, E extends Throwable> {
    // Declare you throw E, whatever that is.
    R apply(T t) throws E;
}   

interface Stream<T> {
    // Pass through E, whatever mapper defined for E.
    <R, E extends Throwable> Stream<R> map(Function<? super T, ? extends R, E> mapper) throws E;
}   

class Main {
    public static void main(final String... args) throws ClassNotFoundException {
        final Stream<String> s = null;

        // Works: E is ClassNotFoundException.
        s.map(Class::forName);

        // Works: E is RuntimeException (probably).
        s.map(Main::convertClass);

        // Works: E is ClassNotFoundException.
        s.map(Main::throwSome);

        // Doesn't work: E is Exception.
        s.map(Main::throwSomeMore);  // error: unreported exception Exception; must be caught or declared to be thrown
    }   

    public static Class convertClass(final String s) {
        return Main.class;
    }   

    static class FooException extends ClassNotFoundException {}

    static class BarException extends ClassNotFoundException {}

    public static Class throwSome(final String s) throws FooException, BarException {
        throw new FooException();
    }   

    public static Class throwSomeMore(final String s) throws ClassNotFoundException, IOException  {
        throw new FooException();
    }   
}   

In the case of throwSomeMore we would like to see IOException being missed, but it actually misses Exception.

This is not perfect because type inference seems to be looking for a single type, even in the case of exceptions. Because the type inference needs a single type, E needs to resolve to a common super of ClassNotFoundException and IOException, which is Exception.

A tweak to the definition of type inference is needed so that the compiler would look for multiple types if the type parameter is used where a list of types is permissible (throws clause). Then the exception type reported by the compiler would be as specific as the original throws declaration of the checked exceptions of the referenced method, not a single catch-all super type.

The bad news is that this means that Oracle messed it up. Certainly they won't break user-land code, but introducing exception type parameters to the existing functional interfaces would break compilation of all user-land code that uses these interfaces explicitly. They'll have to invent some new syntax sugar to fix this.

The even worse news is that this topic was already discussed by Brian Goetz in 2010 https://blogs.oracle.com/briangoetz/entry/exception_transparency_in_java (new link: http://mail.openjdk.java.net/pipermail/lambda-dev/2010-June/001484.html) but I'm informed that this investigation ultimately did not pan out, and that there is no current work at Oracle that I know of to mitigate the interactions between checked exceptions and lambdas.