我在尝试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;
    ....
}

问题是,它不能编译,因为我必须捕获isActive-和getNumber-Methods的可能异常。但是,即使我显式地使用如下所示的try-catch-Block,它仍然不能编译,因为我没有捕获异常。所以,要么是JDK有bug,要么是我不知道如何捕捉这些异常。

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){
        }
    }
    ....
}

我怎样才能让它工作呢?谁能给我点提示吗?


当前回答

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


细节:

这并没有直接回答问题(有很多其他的答案),但试图从一开始就避免这个问题:

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

interface Account {
    boolean isActive() throws IOException;
    String getNumber() throws IOException;
}

不要在每个getter上抛出IOException,考虑这样的设计:

interface AccountReader {
    Account readAccount(…) throws IOException;
}

interface Account {
    boolean isActive();
    String getNumber();
}

方法AccountReader.readAccount(…)可以从数据库或文件中读取帐户,如果未成功则抛出异常。它构造一个Account对象,该对象已经包含所有值,可以随时使用。由于这些值已经被readAccount(…)加载,getter不会抛出异常。因此,你可以在lambdas中自由地使用它们,而不需要包装、屏蔽或隐藏异常。

注意,您仍然需要处理readAccount(…)抛出的异常。毕竟,这就是异常存在的首要原因。但是假设readAccount(…)是在“其他地方”使用的,即在lambdas之外,在那里你可以使用Java提供的“正常”异常处理机制,即try-catch来处理它或throws来让它“冒泡”。

当然,不可能总是按照我描述的方式来做,但通常是这样的,它会导致更干净的代码(恕我直言):

Better separation of concerns and following single responsibility principle Less boilerplate: You don't have to clutter your code with throws IOException for no use but to satisfy the compiler Error handling: You handle the errors where they happen - when reading from a file or database - instead of somewhere in the middle of your business logic only because you want to get a fields value You may be able to make Account immutable and profit from the advantages thereof (e.g. thread safety) You don't need "dirty tricks" or workarounds to use Account in lambdas (e.g. in a Stream)

其他回答

你可以通过包装你的lambda来抛出一个未检查的异常,然后在终端操作中解开这个未检查的异常,从而滚动你自己的Stream变体:

@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));
    }
}

然后你可以像使用流一样使用它:

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());

这个解决方案需要相当多的样板文件,所以我建议您看一看我已经创建的库,它完全符合我在这里为整个Stream类(以及更多!)所描述的内容。

你必须在异常转义lambda之前捕获它:

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

考虑到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);
    }
}

你的例子可以写成

return s.filter(a -> uncheckCall(a::isActive))
        .map(Account::getNumber)
        .collect(toSet());

在我的项目中,我不带包装地处理这个问题;相反,我使用一种方法,有效地化解编译器的异常检查。不用说,这应该小心处理,项目中的每个人都必须意识到,在未声明的地方可能会出现受控异常。这是管道代码:

public static <T> T uncheckCall(Callable<T> callable) {
    try {
        return callable.call();
    } catch (Exception e) {
        sneakyThrow(e);
        return null; // Unreachable but needed to satisfy compiler
    }
}

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;
}

and you can expect to get an IOException thrown in your face, even though collect does not declare it. In most, but not all real-life cases you would want to just rethrow the exception, anyway, and handle it as a generic failure. In all those cases, nothing is lost in clarity or correctness. Just beware of those other cases, where you would actually want to react to the exception on the spot. The developer will not be made aware by the compiler that there is an IOException to catch there and the compiler will in fact complain if you try to catch it because we have fooled it into believing that no such exception can be thrown.

它可以通过以下简单的代码在abacus-common中使用Stream和Try来解决:

Stream.of(accounts)
      .filter(a -> Try.call(a::isActive))
      .map(a -> Try.call(a::getNumber))
      .toSet();

Or:

Stream.of(accounts)
      .filterE(Account::isActive)
      .mapE(Account::getNumber)
      .toSet();

披露:我是abacus-common的开发者。

如果你不介意使用第三方库,AOL的cyclops-react库,disclosure::我是一个贡献者,有一个ExceptionSoftener类可以在这里提供帮助。

 s.filter(softenPredicate(a->a.isActive()));

也可以使用一些外部(流)错误指示器在更高级别抛出异常:

List<String> errorMessages = new ArrayList<>(); // error indicator
//..
errorMessages.clear();

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

names.stream()
.map(name -> {
    if (name != "pamela") {
      errorMessages.add(name + " is wrong here!"); 
      return null; // triggering the indicator
    }
    return name;
} )
.filter(elem -> (elem != null)) // bypassing propagation of only current unwanted data
//.filter(elem -> (errorMessages.size() == 0)) // or blocking any propagation once unwanted data detected
.forEach(System.out::println);

if (errorMessages.size() > 0) { // handling the indicator
  throw  new RuntimeException(String,join(", ", errorMessages));
}