Java 8:Lambda-Streams,按方法过滤,具有异常

Mar*_*ber 161 java lambda exception-handling java-8

我在尝试Java 8的Lambda表达式时遇到了问题.通常它工作正常,但现在我有方法可以抛出IOException.最好看一下以下代码:

class Bank{
    ....
    public Set<String> getActiveAccountNumbers() throws IOException {
        Stream<Account> s =  accounts.values().stream();
        s = s.filter(a -> a.isActive());
        Stream<String> ss = s.map(a -> a.getNumber());
        return ss.collect(Collectors.toSet());
    }
    ....
}

interface Account{
    ....
    boolean isActive() throws IOException;
    String getNumber() throws IOException;
    ....
}
Run Code Online (Sandbox Code Playgroud)

问题是,它不能编译,因为我必须捕获isActive-和getNumber-Methods的可能例外.但即使我明确使用如下所示的try-catch-Block,它仍然无法编译,因为我没有捕获异常.所以要么JDK中存在错误,要么我不知道如何捕获这些异常.

class Bank{
    ....
    //Doesn't compile either
    public Set<String> getActiveAccountNumbers() throws IOException {
        try{
            Stream<Account> s =  accounts.values().stream();
            s = s.filter(a -> a.isActive());
            Stream<String> ss = s.map(a -> a.getNumber());
            return ss.collect(Collectors.toSet());
        }catch(IOException ex){
        }
    }
    ....
}
Run Code Online (Sandbox Code Playgroud)

我怎样才能让它发挥作用?有人能暗示我找到正确的解决方案吗?

Mar*_*nik 203

你必须它逃脱lambda 之前捕获异常:

s = s.filter(a -> { try { return a.isActive(); } 
                    catch (IOException e) { throw new UncheckedIOException(e); }}});
Run Code Online (Sandbox Code Playgroud)

考虑这样一个事实:lambda不是在你编写它的地方进行评估,而是在JDK类中的一些完全不相关的地方进行评估.因此,这将是抛出已检查异常的点,并且在该位置不会声明它.

您可以使用lambda的包装器来处理它,该包装器将已检查的异常转换为未经检查的异常:

public static <T> T uncheckCall(Callable<T> callable) {
  try { return callable.call(); }
  catch (RuntimeException e) { throw e; }
  catch (Exception e) { throw new RuntimeException(e); }
}
Run Code Online (Sandbox Code Playgroud)

你的例子将被写为

return s.filter(a -> uncheckCall(a::isActive))
        .map(Account::getNumber)
        .collect(toSet());
Run Code Online (Sandbox Code Playgroud)

在我的项目中,我没有包装就处理这个问题; 相反,我使用的方法有效地解除了编译器对异常的检查.毋庸置疑,应谨慎处理,项目中的每个人都必须意识到,未声明的情况下可能会出现已检查的异常.这是管道代码:

public static <T> T uncheckCall(Callable<T> callable) {
  try { return callable.call(); }
  catch (Exception e) { return sneakyThrow(e); }
}
public static void uncheckRun(RunnableExc r) {
  try { r.run(); } catch (Exception e) { sneakyThrow(e); }
}
public interface RunnableExc { void run() throws Exception; }


@SuppressWarnings("unchecked")
private static <T extends Throwable> void sneakyThrow(Throwable t) throws T {
  throw (T) t;
}
Run Code Online (Sandbox Code Playgroud)

IOException即使collect没有声明,你也可能会被抛到脸上.在大多数情况下,但不是所有现实情况下,您都希望重新抛出异常,并将其作为一般故障处理.在所有这些情况下,在清晰度或正确性方面没有任何损失.请注意其他情况,您实际上想要在现场对异常作出反应.开发人员不会被编译器意识到有一个IOException要捕获的东西,如果你试图捕获它,编译器实际上会抱怨它,因为我们已经欺骗它,因为我们相信不会抛出这样的异常.

  • @brian只是因为某事是规则并不意味着这是一个好主意.但是我很惊讶你将我的答案的第二部分称为"建议",因为我认为我明确表示我建议作为一种解决方案,以及我作为一个FYI向感兴趣的读者提供的内容,以及大量的免责声明. (30认同)
  • 仅仅因为你不喜欢这些规则,并不意味着将法律掌握在自己手中是一个好主意.您的建议是不负责任的,因为它将代码编写者的便利性放在了程序透明度和可维护性这一更为重要的考虑因素上. (28认同)
  • 没有理智的API应该向客户端提供未声明的已检查异常,这是肯定的._Within_ API可以理解,检查异常可能会泄漏.它们不会造成伤害,只要它们只是一般失败的另一个迹象,即将被批发捕获和处理. (8认同)
  • @kevinarpe这就是为什么偷偷摸摸的投掷是一个坏主意的确切原因.短路编译器必然会混淆未来的维护者. (5认同)
  • 我以前见过NettyIO做过"偷偷摸摸",我想把椅子扔到窗外."什么?检查异常泄漏的位置是什么?" 这是我见过的第一个偷偷摸摸的合法用例.作为一名程序员,你必须保持警惕,表明偷偷摸摸是可能的.也许更好的只是创建另一个支持已检查异常的流接口/ impl? (4认同)
  • @ThorbjørnRavnAndersen 检查异常是另一个坏主意,尤其是在 lambda 主体中。如果不是这样,没有人会考虑偷偷摸摸的投掷。 (2认同)
  • “sneakyThrow”方法可以在“apache.commons.lang3.exception.ExceptionUtils”中以“typeErasure”名称找到。如果可以的话请使用它,这样*您*就不必成为将 `sneakyThrow` 添加到您的项目的提交者。;-) (2认同)

And*_*hev 29

你也可以用lambdas传播你的静态疼痛,所以整个事情看起来很可读:

s.filter(a -> propagate(a::isActive))
Run Code Online (Sandbox Code Playgroud)

propagate这里接收java.util.concurrent.Callable作为参数并将调用期间捕获的任何异常转换为RuntimeException.在番石榴中有一种类似的转换方法Throwables#propagate(Throwable).

这种方法对于lambda方法链接似乎是必不可少的,所以我希望有一天它会被添加到一个流行的lib中,或者这种传播行为将是默认的.

public class PropagateExceptionsSample {
    // a simplified version of Throwables#propagate
    public static RuntimeException runtime(Throwable e) {
        if (e instanceof RuntimeException) {
            return (RuntimeException)e;
        }

        return new RuntimeException(e);
    }

    // this is a new one, n/a in public libs
    // Callable just suits as a functional interface in JDK throwing Exception 
    public static <V> V propagate(Callable<V> callable){
        try {
            return callable.call();
        } catch (Exception e) {
            throw runtime(e);
        }
    }

    public static void main(String[] args) {
        class Account{
            String name;    
            Account(String name) { this.name = name;}

            public boolean isActive() throws IOException {
                return name.startsWith("a");
            }
        }


        List<Account> accounts = new ArrayList<>(Arrays.asList(new Account("andrey"), new Account("angela"), new Account("pamela")));

        Stream<Account> s = accounts.stream();

        s
          .filter(a -> propagate(a::isActive))
          .map(a -> a.name)
          .forEach(System.out::println);
    }
}
Run Code Online (Sandbox Code Playgroud)


Mar*_*rcG 20

这个UtilException帮助器类允许您在Java流中使用任何已检查的异常,如下所示:

Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
      .map(rethrowFunction(Class::forName))
      .collect(Collectors.toList());
Run Code Online (Sandbox Code Playgroud)

注意Class::forName抛出ClassNotFoundException,检查.流本身也会抛出ClassNotFoundException,而不是一些包装未经检查的异常.

public final class UtilException {

@FunctionalInterface
public interface Consumer_WithExceptions<T, E extends Exception> {
    void accept(T t) throws E;
    }

@FunctionalInterface
public interface BiConsumer_WithExceptions<T, U, E extends Exception> {
    void accept(T t, U u) throws E;
    }

@FunctionalInterface
public interface Function_WithExceptions<T, R, E extends Exception> {
    R apply(T t) throws E;
    }

@FunctionalInterface
public interface Supplier_WithExceptions<T, E extends Exception> {
    T get() throws E;
    }

@FunctionalInterface
public interface Runnable_WithExceptions<E extends Exception> {
    void run() throws E;
    }

/** .forEach(rethrowConsumer(name -> System.out.println(Class.forName(name)))); or .forEach(rethrowConsumer(ClassNameUtil::println)); */
public static <T, E extends Exception> Consumer<T> rethrowConsumer(Consumer_WithExceptions<T, E> consumer) throws E {
    return t -> {
        try { consumer.accept(t); }
        catch (Exception exception) { throwAsUnchecked(exception); }
        };
    }

public static <T, U, E extends Exception> BiConsumer<T, U> rethrowBiConsumer(BiConsumer_WithExceptions<T, U, E> biConsumer) throws E {
    return (t, u) -> {
        try { biConsumer.accept(t, u); }
        catch (Exception exception) { throwAsUnchecked(exception); }
        };
    }

/** .map(rethrowFunction(name -> Class.forName(name))) or .map(rethrowFunction(Class::forName)) */
public static <T, R, E extends Exception> Function<T, R> rethrowFunction(Function_WithExceptions<T, R, E> function) throws E {
    return t -> {
        try { return function.apply(t); }
        catch (Exception exception) { throwAsUnchecked(exception); return null; }
        };
    }

/** rethrowSupplier(() -> new StringJoiner(new String(new byte[]{77, 97, 114, 107}, "UTF-8"))), */
public static <T, E extends Exception> Supplier<T> rethrowSupplier(Supplier_WithExceptions<T, E> function) throws E {
    return () -> {
        try { return function.get(); }
        catch (Exception exception) { throwAsUnchecked(exception); return null; }
        };
    }

/** uncheck(() -> Class.forName("xxx")); */
public static void uncheck(Runnable_WithExceptions t)
    {
    try { t.run(); }
    catch (Exception exception) { throwAsUnchecked(exception); }
    }

/** uncheck(() -> Class.forName("xxx")); */
public static <R, E extends Exception> R uncheck(Supplier_WithExceptions<R, E> supplier)
    {
    try { return supplier.get(); }
    catch (Exception exception) { throwAsUnchecked(exception); return null; }
    }

/** uncheck(Class::forName, "xxx"); */
public static <T, R, E extends Exception> R uncheck(Function_WithExceptions<T, R, E> function, T t) {
    try { return function.apply(t); }
    catch (Exception exception) { throwAsUnchecked(exception); return null; }
    }

@SuppressWarnings ("unchecked")
private static <E extends Throwable> void throwAsUnchecked(Exception exception) throws E { throw (E)exception; }

}
Run Code Online (Sandbox Code Playgroud)

关于如何使用它的许多其他示例(静态导入后UtilException):

@Test
public void test_Consumer_with_checked_exceptions() throws IllegalAccessException {
    Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
          .forEach(rethrowConsumer(className -> System.out.println(Class.forName(className))));

    Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
          .forEach(rethrowConsumer(System.out::println));
    }

@Test
public void test_Function_with_checked_exceptions() throws ClassNotFoundException {
    List<Class> classes1
          = Stream.of("Object", "Integer", "String")
                  .map(rethrowFunction(className -> Class.forName("java.lang." + className)))
                  .collect(Collectors.toList());

    List<Class> classes2
          = Stream.of("java.lang.Object", "java.lang.Integer", "java.lang.String")
                  .map(rethrowFunction(Class::forName))
                  .collect(Collectors.toList());
    }

@Test
public void test_Supplier_with_checked_exceptions() throws ClassNotFoundException {
    Collector.of(
          rethrowSupplier(() -> new StringJoiner(new String(new byte[]{77, 97, 114, 107}, "UTF-8"))),
          StringJoiner::add, StringJoiner::merge, StringJoiner::toString);
    }

@Test    
public void test_uncheck_exception_thrown_by_method() {
    Class clazz1 = uncheck(() -> Class.forName("java.lang.String"));

    Class clazz2 = uncheck(Class::forName, "java.lang.String");
    }

@Test (expected = ClassNotFoundException.class)
public void test_if_correct_exception_is_still_thrown_by_method() {
    Class clazz3 = uncheck(Class::forName, "INVALID");
    }
Run Code Online (Sandbox Code Playgroud)

但在了解以下优点,缺点和限制之前,请不要使用它:

•如果调用代码要处理已检查的异常,则必须将其添加到包含该流的方法的throws子句中.编译器不会强迫你再添加它,因此更容易忘记它.

•如果调用代码已经处理了已检查的异常,编译器将提醒您将throws子句添加到包含流的方法声明中(如果不这样做,则会说:异常永远不会在相应的try语句的主体中抛出).

•在任何情况下,您将无法包围流本身以捕获已检查的异常INSIDE包含流的方法(如果您尝试,编译器将说:异常永远不会在相应的try语句的主体中抛出).

•如果您正在调用一个字面上永远不会抛出它声明的异常的方法,那么您不应该包含throws子句.例如:new String(byteArr,"UTF-8")抛出UnsupportedEncodingException,但Java规范保证UTF-8始终存在.在这里,投掷声明是一个麻烦,任何解决方案,以最小的样板沉默它是受欢迎的.

•如果你讨厌检查异常,并认为它们永远不应该被添加到Java语言中(越来越多的人这样认为,而我不是其中之一),那么就不要将已检查的异常添加到throws包含流的方法的子句.然后,检查的异常将表现得像UNchecked异常.

•如果您正在实现一个严格的接口,其中您没有添加throws声明的选项,但抛出异常是完全合适的,那么为了获得抛出它的特权而包装异常会导致带有虚假异常的堆栈跟踪没有提供实际出错的信息.一个很好的例子是Runnable.run(),它不会抛出任何已检查的异常.在这种情况下,您可以决定不将已检查的异常添加到包含该流的方法的throws子句中.

•在任何情况下,如果您决定不将(但忘记添加)已检查的异常添加到包含流的方法的throws子句中,请注意抛出CHECKED异常的这两个后果:

1)调用代码将无法通过名称捕获它(如果您尝试,编译器将说:异常永远不会在相应的try语句的主体中抛出).它会冒泡并可能​​在主程序循环中被一些"捕获异常"或"捕获Throwable"捕获,这可能是你想要的.

2)它违反了最少惊喜的原则:捕获RuntimeException以便能够保证捕获所有可能的异常将不再足够.出于这个原因,我认为这不应该在框架代码中完成,而只能在您完全控制的业务代码中完成.

总结:我认为这里的局限并不严重,UtilException可以毫无畏惧地使用该课程.但是,这取决于你!


Jef*_*rey 8

您可以Stream通过包装lambda来抛出一个未经检查的异常,然后在终端操作中展开该未经检查的异常,从而推出自己的变体:

@FunctionalInterface
public interface ThrowingPredicate<T, X extends Throwable> {
    public boolean test(T t) throws X;
}

@FunctionalInterface
public interface ThrowingFunction<T, R, X extends Throwable> {
    public R apply(T t) throws X;
}

@FunctionalInterface
public interface ThrowingSupplier<R, X extends Throwable> {
    public R get() throws X;
}

public interface ThrowingStream<T, X extends Throwable> {
    public ThrowingStream<T, X> filter(
            ThrowingPredicate<? super T, ? extends X> predicate);

    public <R> ThrowingStream<T, R> map(
            ThrowingFunction<? super T, ? extends R, ? extends X> mapper);

    public <A, R> R collect(Collector<? super T, A, R> collector) throws X;

    // etc
}

class StreamAdapter<T, X extends Throwable> implements ThrowingStream<T, X> {
    private static class AdapterException extends RuntimeException {
        public AdapterException(Throwable cause) {
            super(cause);
        }
    }

    private final Stream<T> delegate;
    private final Class<X> x;

    StreamAdapter(Stream<T> delegate, Class<X> x) {
        this.delegate = delegate;
        this.x = x;
    }

    private <R> R maskException(ThrowingSupplier<R, X> method) {
        try {
            return method.get();
        } catch (Throwable t) {
            if (x.isInstance(t)) {
                throw new AdapterException(t);
            } else {
                throw t;
            }
        }
    }

    @Override
    public ThrowingStream<T, X> filter(ThrowingPredicate<T, X> predicate) {
        return new StreamAdapter<>(
                delegate.filter(t -> maskException(() -> predicate.test(t))), x);
    }

    @Override
    public <R> ThrowingStream<R, X> map(ThrowingFunction<T, R, X> mapper) {
        return new StreamAdapter<>(
                delegate.map(t -> maskException(() -> mapper.apply(t))), x);
    }

    private <R> R unmaskException(Supplier<R> method) throws X {
        try {
            return method.get();
        } catch (AdapterException e) {
            throw x.cast(e.getCause());
        }
    }

    @Override
    public <A, R> R collect(Collector<T, A, R> collector) throws X {
        return unmaskException(() -> delegate.collect(collector));
    }
}
Run Code Online (Sandbox Code Playgroud)

然后您可以使用与以下相同的方式Stream:

Stream<Account> s = accounts.values().stream();
ThrowingStream<Account, IOException> ts = new StreamAdapter<>(s, IOException.class);
return ts.filter(Account::isActive).map(Account::getNumber).collect(toSet());
Run Code Online (Sandbox Code Playgroud)

这个解决方案需要相当多的样板,所以我建议你看看我已经制作,这完全是我在这里描述的整个Stream类(以及更多!).


sie*_*egi 6

TLDR:尝试通过重构代码来避免该问题:将“容易出错”的操作与“安全”的操作分开,只在 lambda 中使用安全的操作。

\n
\n

细节:

\n

这并没有直接回答这个问题(还有许多其他答案可以回答),而是试图首先避免这个问题:

\n

根据我的经验,处理 a Stream(或其他 lambda 表达式)中的异常的需要通常来自这样一个事实:异常被声明为从不应抛出异常的方法中抛出。这通常来自于将业务逻辑与输入和输出混合在一起。你的Account界面就是一个完美的例子:

\n
interface Account {\n    boolean isActive() throws IOException;\n    String getNumber() throws IOException;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

不要IOException在每个 getter 上添加一个,而是考虑以下设计:

\n
interface AccountReader {\n    Account readAccount(\xe2\x80\xa6) throws IOException;\n}\n\ninterface Account {\n    boolean isActive();\n    String getNumber();\n}\n
Run Code Online (Sandbox Code Playgroud)\n

该方法AccountReader.readAccount(\xe2\x80\xa6)可以从数据库或文件或其他任何内容读取帐户,如果不成功则抛出异常。它构造一个Account已经包含所有值的对象,可供使用。由于这些值已经被加载readAccount(\xe2\x80\xa6),getter 不会抛出异常。因此,您可以在 lambda 中自由使用它们,而无需包装、屏蔽或隐藏异常。

\n

请注意,您仍然需要处理readAccount(\xe2\x80\xa6). 毕竟,这就是例外存在的首要原因。但假设是在readAccount(\xe2\x80\xa6)“其他地方”使用,即在 lambda 之外,您可以使用 Java 提供的“正常”异常处理机制,即try-catch处理它或throws让它“冒泡”。

\n

当然,并不总是可以按照我描述的方式做到这一点,但通常是这样,并且它会导致完全更干净的代码(恕我直言):

\n
    \n
  • 更好地分离关注点并遵循单一职责原则
  • \n
  • 更少的样板文件:您不必throws IOException为了满足编译器的要求而使代码变得混乱
  • \n
  • 错误处理:您可以在发生错误时处理错误(从文件或数据库读取时),而不是在业务逻辑中间的某个位置处理,因为您想要获取字段值
  • \n
  • 您也许能够实现Account 不可变并从中受益(例如线程安全)
  • \n
  • 您不需要Account在 lambda 中使用“肮脏技巧”或解决方法(例如在 a 中Stream
  • \n
\n


小智 5

使用#propagate()方法.Sam BeranJava 8 Blog中获取非Guava实现示例:

public class Throwables {
    public interface ExceptionWrapper<E> {
        E wrap(Exception e);
    }

    public static <T> T propagate(Callable<T> callable) throws RuntimeException {
        return propagate(callable, RuntimeException::new);
    }

    public static <T, E extends Throwable> T propagate(Callable<T> callable, ExceptionWrapper<E> wrapper) throws E {
        try {
            return callable.call();
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw wrapper.wrap(e);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)