这里写目录标题
处理异常代码块
Stream API 和 lambda 是自第 8 版以来 Java 中的重大改进。从那时起,我们可以使用更具功能性的语法样式。现在,在使用这些代码结构几年后,仍然存在的较大问题之一是如何处理 lambda 中的已检查异常。
众所周知,不可能直接从 lambda 调用抛出已检查异常的方法。在某种程度上,我们需要捕获异常以使代码编译。自然,我们可以在 lambda 中执行一个简单的 try-catch 并将异常包装到 a 中 RuntimeException,如第一个示例所示,但这并不是最好的方法。
myList.stream()
.map(item -> {
try {
return doSomething(item);
} catch (MyException e) {
throw new RuntimeException(e);
}
})
.forEach(System.out::println);
大多数人都知道这个 lambda块 很笨重且可读性较差。我们应该尽可能避免。如果我们需要做的不止一行,我们可以将函数体提取到一个单独的方法中,然后简单地调用新方法。解决这个问题的更好、更易读的方法是将调用包装在一个普通的方法中,该方法执行 try-catch 并从 lambda 中调用该方法。
myList.stream()
.map(this::trySomething)
.forEach(System.out::println);
private Item trySomething(Item item) {
try {
return doSomething(item);
} catch (MyException e) {
throw new RuntimeException(e);
}
}
这个解决方案至少更具可读性,我们确实将我们的关注点分开了。如果您真的想捕获异常并做一些特定的事情,而不是简单地将异常包装到 RuntimeException中,这对您来说可能是一个可行的解决方案。
运行时异常
在许多情况下,您会看到人们使用这些类型的解决方案将异常重新打包成一个 RuntimeException或更具体的未检查异常的实现。通过这样做,可以在 lambda 内部调用该方法并在高阶函数中使用。
我可以将这种做法与这种做法联系起来,因为我个人认为一般来说检查异常没有太大价值,但这是另一个我不打算从这里开始的讨论。如果您想将每个调用都包装在一个 lambda 中,该 lambda 已检查为 RuntimeException,您将看到您重复相同的模式。为了避免一遍又一遍地重写相同的代码,为什么不把它抽象成一个实用函数呢?这样,您只需编写一次并在每次需要时调用它。
为此,您首先需要为函数编写自己的函数式接口版本
@FunctionalInterface
public interface CheckedFunction<T,R> {
R apply(T t) throws Exception;
}
现在,您已准备好编写自己的通用实用程序函数,该函数接受CheckedFunction。您可以在此实用程序函数中处理 try-catch 并将原始异常包装到 RuntimeException或其他一些未经检查的异常中。
public static <T,R> Function<T,R> wrap(CheckedFunction<T,R> checkedFunction) {
return t -> {
try {
return checkedFunction.apply(t);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
myList.stream()
.map(wrap(item -> doSomething(item)))
.forEach(System.out::println);
剩下的唯一问题是,当发生异常时,流的处理会立即停止。在许多情况下,直接终止并不理想。
Either处理Stream异常
在处理流时,如果发生异常,我们可能不想停止处理流。
让我们换个思路。为什么不像考虑“成功”结果一样尽可能多地考虑“特殊情况”。让我们将其视为数据,继续处理流,然后决定如何处理它。我们可以做到这一点,但需要引入一种新类型——Either 类型。
类似于 Java 中的 Optional 类型,Either是一个具有两种可能性的通用包装器。它可以是左或右,但不能同时是两者。left 和 right 都可以是任何类型。例如,如果我们有一个Either 值,则该值可以包含String 类型或Integer, 类型的内容Either<String,Integer>。
如果我们将这个原则用于异常处理,我们可以说我们的 Either 类型持有一个 Exception或一个值。为方便起见,通常左边是异常值,右边是成功值。
Either 类型的基本实现 :
public class Either<L, R> {
private final L left;
private final R right;
private Either(L left, R right) {
this.left = left;
this.right = right;
}
public static <L,R> Either<L,R> Left( L value) {
return new Either(value, null);
}
public static <L,R> Either<L,R> Right( R value) {
return new Either(null, value);
}
public Optional<L> getLeft() {
return Optional.ofNullable(left);
}
public Optional<R> getRight() {
return Optional.ofNullable(right);
}
public boolean isLeft() {
return left != null;
}
public boolean isRight() {
return right != null;
}
public <T> Optional<T> mapLeft(Function<? super L, T> mapper) {
if (isLeft()) {
return Optional.of(mapper.apply(left));
}
return Optional.empty();
}
public <T> Optional<T> mapRight(Function<? super R, T> mapper) {
if (isRight()) {
return Optional.of(mapper.apply(right));
}
return Optional.empty();
}
public String toString() {
if (isLeft()) {
return "Left(" + left +")";
}
return "Right(" + right +")";
}
}
你现在可以让自己的函数返回一个 Either 而不是抛出一个 Exception。但是,如果你想使用Exception 在 lambda中抛出异常,这对个方法还需要些改进,因此,我们必须为Either 做点改动 。
public static <T,R> Function<T, Either> lift(CheckedFunction<T,R> function) {
return t -> {
try {
return Either.Right(function.apply(t));
} catch (Exception ex) {
return Either.Left(ex);
}
};
}
通过将这个静态方法添加到 Either,我们现在可以简单地抛出已检查异常的函数并让它返回一个 Either。最终会得到一个 Stream ,而不是一个可能 RuntimeException 就会炸毁我整个 Stream。
这只是意味着我们已经收回了控制权。通过使用 Stream 中的过滤器功能,我们可以简单地过滤掉左边的实例,然后记录它们或者做其他的处理。你还可以过滤正确的实例并简单地忽略其他情况。无论哪种方式,您都将重新获得控制权,并且您的流不会因RuntimeException发就立即终止 。
因为 Either 是通用包装器,所以它可以用于任何类型,而不仅仅是用于异常处理。这让我们有机会做更多的事情,而不仅仅是将Exception包装到 Either。我们现在可能遇到的问题是,如果 Either 仅仅持有包装的异常,我们无法重试,因为我们丢失了原始值。通过使用Either 保存任何东西的能力 ,我们可以将异常和值都存储在左值中。为此,我们需要给Either来点小小的增强。
public static <T,R> Function<T, Either> liftWithValue(CheckedFunction<T,R> function) {
return t -> {
try {
return Either.Right(function.apply(t));
} catch (Exception ex) {
return Either.Left(Pair.of(ex,t));
}
};
}
在此 liftWithValue 函数中, Pair 类型用于将异常和原始值配对到Either,现在左侧如果出现问题,我们将拥有需要的所有信息,而不是只有 Exception。
Pair 这里使用的 类型是另一种可以在 Apache Commons lang 库中找到的泛型类型,或者您可以简单地实现自己的类型。无论如何,它只是一种可以容纳两个值的类型。
public class Pair<F,S> {
public final F fst;
public final S snd;
private Pair(F fst, S snd) {
this.fst = fst;
this.snd = snd;
}
public static <F,S> Pair<F,S> of(F fst, S snd) {
return new Pair<>(fst,snd);
}
}
通过使用 liftWithValue,您现在可以灵活地控制使用可能Exception 在 lambda 内部抛出 的方法 。当 Either 是right的时候,我们知道函数被正确应用,我们可以提取结果。另一方面,如果 Either 是left的时候,我们就知道出了点问题,我们可以同时提取Exception值和原始值,因此我们可以随心所欲地进行。通过使用 Either 类型而不是将已检查的包装 Exception 到 RuntimeException中,我们可以防止 Stream 中途终止。
结束语
当你想使用一个抛出异常的方法时, 如果你想在 lambda 中调用它checkedException,你必须做一些额外的事情。将它包装成 一个RuntimeException 一种解决方案。如果您更喜欢使用这种方法,我强烈建议您创建一个简单的包装器工具并重复使用它,这样您就不会每次try/catch。
如果你想拥有更多的控制权,你可以使用 Either类型来包装函数的结果,这样你就可以将它作为一条数据来处理。抛出RuntimeException时流不会终止,您可以随意处理流中的数据