Java Streams 中的异常处理

这里写目录标题

处理异常代码块

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时流不会终止,您可以随意处理流中的数据

上一篇:Junit单元测试——如何正确测试异常


下一篇:超级简单Android 拦截app崩溃,并且把崩溃写入本地保存文件