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

前端 未结 18 1601
你的背包
你的背包 2020-11-22 06:59

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

        
相关标签:
18条回答
  • 2020-11-22 07:42

    This answer is similar to 17 but avoiding wrapper exception definition:

    List test = new ArrayList();
            try {
                test.forEach(obj -> {
    
                    //let say some functionality throws an exception
                    try {
                        throw new IOException("test");
                    }
                    catch(Exception e) {
                        throw new RuntimeException(e);
                    }
                });
            }
            catch (RuntimeException re) {
                if(re.getCause() instanceof IOException) {
                    //do your logic for catching checked
                }
                else 
                    throw re; // it might be that there is real runtime exception
            }
    
    0 讨论(0)
  • 2020-11-22 07:42

    The only built-in way of handling checked exceptions that can be thrown by a map operation is to encapsulate them within a CompletableFuture. (An Optional is a simpler alternative if you don't need to preserve the exception.) These classes are intended to allow you to represent contingent operations in a functional way.

    A couple of non-trivial helper methods are required, but you can arrive at code that's relatively concise, while still making it apparent that your stream's result is contingent on the map operation having completed successfully. Here's what it looks like:

        CompletableFuture<List<Class<?>>> classes =
                Stream.of("java.lang.String", "java.lang.Integer", "java.lang.Double")
                      .map(MonadUtils.applyOrDie(Class::forName))
                      .map(cfc -> cfc.thenApply(Class::getSuperclass))
                      .collect(MonadUtils.cfCollector(ArrayList::new,
                                                      List::add,
                                                      (List<Class<?>> l1, List<Class<?>> l2) -> { l1.addAll(l2); return l1; },
                                                      x -> x));
        classes.thenAccept(System.out::println)
               .exceptionally(t -> { System.out.println("unable to get class: " + t); return null; });
    

    This produces the following output:

    [class java.lang.Object, class java.lang.Number, class java.lang.Number]
    

    The applyOrDie method takes a Function that throws an exception, and converts it into a Function that returns an already-completed CompletableFuture -- either completed normally with the original function's result, or completed exceptionally with the thrown exception.

    The second map operation illustrates that you've now got a Stream<CompletableFuture<T>> instead of just a Stream<T>. CompletableFuture takes care of only executing this operation if the upstream operation succeeded. The API makes this explict, but relatively painless.

    Until you get to the collect phase, that is. This is where we require a pretty significant helper method. We want to "lift" a normal collection operation (in this case, toList()) "inside" the CompletableFuture -- cfCollector() lets us do that using a supplier, accumulator, combiner, and finisher that don't need to know anything at all about CompletableFuture.

    The helper methods can be found on GitHub in my MonadUtils class, which is very much still a work in progress.

    0 讨论(0)
  • 2020-11-22 07:43

    You can also write a wrapper method to wrap unchecked exceptions, and even enhance wrapper with additional parameter representing another functional interface (with the same return type R). In this case you can pass a function that would be executed and returned in case of exceptions. See example below:

    private void run() {
        List<String> list = Stream.of(1, 2, 3, 4).map(wrapper(i ->
                String.valueOf(++i / 0), i -> String.valueOf(++i))).collect(Collectors.toList());
        System.out.println(list.toString());
    }
    
    private <T, R, E extends Exception> Function<T, R> wrapper(ThrowingFunction<T, R, E> function, 
    Function<T, R> onException) {
        return i -> {
            try {
                return function.apply(i);
            } catch (ArithmeticException e) {
                System.out.println("Exception: " + i);
                return onException.apply(i);
            } catch (Exception e) {
                System.out.println("Other: " + i);
                return onException.apply(i);
            }
        };
    }
    
    @FunctionalInterface
    interface ThrowingFunction<T, R, E extends Exception> {
        R apply(T t) throws E;
    }
    
    0 讨论(0)
  • 2020-11-22 07:43

    Probably, a better and more functional way is to wrap exceptions and propagate them further in the stream. Take a look at the Try type of Vavr for example.

    Example:

    interface CheckedFunction<I, O> {
        O apply(I i) throws Exception; }
    
    static <I, O> Function<I, O> unchecked(CheckedFunction<I, O> f) {
        return i -> {
            try {
                return f.apply(i);
            } catch(Exception ex) {
    
                throw new RuntimeException(ex);
            }
        } }
    
    fileNamesToRead.map(unchecked(file -> Files.readAllLines(file)))
    

    OR

    @SuppressWarnings("unchecked")
    private static <T, E extends Exception> T throwUnchecked(Exception e) throws E {
        throw (E) e;
    }
    
    static <I, O> Function<I, O> unchecked(CheckedFunction<I, O> f) {
        return arg -> {
            try {
                return f.apply(arg);
            } catch(Exception ex) {
                return throwUnchecked(ex);
            }
        };
    }
    

    2nd implementation avoids wrapping the exception in a RuntimeException. throwUnchecked works because almost always all generic exceptions are treated as unchecked in java.

    0 讨论(0)
  • 2020-11-22 07:44

    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.

    0 讨论(0)
  • 2020-11-22 07:46

    You can't do this safely. You can cheat, but then your program is broken and this will inevitably come back to bite someone (it should be you, but often our cheating blows up on someone else.)

    Here's a slightly safer way to do it (but I still don't recommend this.)

    class WrappedException extends RuntimeException {
        Throwable cause;
    
        WrappedException(Throwable cause) { this.cause = cause; }
    }
    
    static WrappedException throwWrapped(Throwable t) {
        throw new WrappedException(t);
    }
    
    try 
        source.stream()
              .filter(e -> { ... try { ... } catch (IOException e) { throwWrapped(e); } ... })
              ...
    }
    catch (WrappedException w) {
        throw (IOException) w.cause;
    }
    

    Here, what you're doing is catching the exception in the lambda, throwing a signal out of the stream pipeline that indicates that the computation failed exceptionally, catching the signal, and acting on that signal to throw the underlying exception. The key is that you are always catching the synthetic exception, rather than allowing a checked exception to leak out without declaring that exception is thrown.

    0 讨论(0)
提交回复
热议问题