java 培训:java 流中的异常处理方法分享
以下文章来源于 IT 码徒
自版本 8 以来,Stream API 和 lambda 是 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);}}
这个解决方案至少更具可读性,我们确实将我们的关注点分开。如果您真的想捕获异常并做一些特定的事情,而不是简单地将异常包装到 a RuntimeException 中,那么这对您来说可能是一个可行且可读的解决方案。
1 运行时异常
在许多情况下,您会看到人们使用这些类型的解决方案将异常重新打包到 RuntimeException 未经检查的异常的一个或更具体的实现中。通过这样做,可以在 lambda 内部调用该方法并在高阶函数中使用该方法。
我可以将这种做法与这种做法联系起来,因为我个人一般认为检查异常没有多大价值,但这是我不打算从这里开始的另一个完整的讨论。如果您想将每个调用包装在一个签入 a 的 lambda 中 RuntimeException,您将看到您重复相同的模式。为了避免一遍又一遍地重写相同的代码,为什么不把它抽象成一个实用函数呢?这样,您只需编写一次并在每次需要时调用它_java培训。
为此,您首先需要为函数编写自己的功能接口版本。只有这一次,您需要定义该函数可能会抛出异常。
@FunctionalInterfacepublic interface CheckedFunction<T,R> {R apply(T t) throws Exception;}
现在,您已准备好编写自己的通用实用程序函数,该实用程序函数接受 CheckedFunction 您刚刚在接口中描述的 a。您可以在此实用程序函数中处理 try-catch 并将原始异常包装到 a RuntimeException(或其他一些未经检查的变体)中。我知道我们现在在这里得到了一个丑陋的块 lambda,你可以从中抽象出身体。如果值得为这个单一实用程序付出努力,请自行选择。
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);}};}
通过简单的静态导入,您现在可以使用全新的实用程序函数包装可能引发异常的 lambda。从这一点开始,一切都会重新开始。
myList.stream().map(wrap(item -> doSomething(item))).forEach(System.out::println);
剩下的唯一问题是,当发生异常时,流的处理会立即停止。如果这对你来说没问题,那就去吧。但是,我可以想象,直接终止在许多情况下并不理想。
2 任何一个
在处理流时,如果发生异常,我们可能不希望停止处理流。如果您的流包含大量需要处理的项目,您是否希望该流在第二个项目引发异常时终止?可能不是。
让我们换个思路。为什么不将“特殊情况”视为可能的结果,就像我们对“成功”结果的考虑一样。让我们将其视为数据,继续处理流,然后决定如何处理它。我们可以做到这一点,但要使其成为可能,我们需要引入一种新类型——Either 类型。
Either 类型是函数式语言中的一种常见类型,而不是(还)Java 的一部分。类似于 Java 中的 Optional 类型,an Either 是具有两种可能性的通用包装器。它可以是左派或右派,但不能两者兼而有之。left 和 right 都可以是任何类型。例如,如果我们有一个 Either 值,则该值可以包含 String 类型或 Integer 类型的东西 Either<String,Integer>。
如果我们使用这个原则来处理异常,我们可以说我们的 Either 类型要么持有一个值,要么持有 Exception 一个值。为方便起见,通常左边是异常值,右边是成功值。你可以记住这一点,把右边不仅是右手边,而且是“好”、“好”等的同义词。
下面,您将看到该类型的基本实现 Either 。在这种情况下, Optional 当我们尝试获取左侧或右侧时,我使用了该类型,因为我们:
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 +")";}}
您现在可以让自己的函数返回 an 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 如果我们考虑最初的问题,我们现在会得到一个 Eithers 流,而不是可能 RuntimeException 会炸毁我的整个 Stream.
myList.stream().map(Either.lift(item -> doSomething(item))).forEach(System.out::println);
这仅仅意味着我们已经收回了控制权。通过使用 Stream APU 中的过滤器功能,我们可以简单地过滤掉左侧的实例,例如记录它们。您还可以过滤正确的实例并简单地忽略异常情况。RuntimeException 无论哪种方式,您都将重新获得控制权,并且当可能发生时,您的流不会立即终止 。
因为 Either 是一个泛型包装器,它可以用于任何类型,而不仅仅是用于异常处理。这使我们有机会做更多的事情,而不仅仅是将 包裹 Exception 到 Either. 我们现在可能遇到的问题是,如果 Either 唯一持有包装的异常,并且我们无法重试,因为我们丢失了原始值。通过使用 Either 保存任何东西的能力,我们可以将异常和值存储在 left 中。为此,我们只需像这样制作第二个静态提升函数。
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 语言库中找到,或者您可以简单地实现自己的。无论如何,它只是一个可以容纳两个值的类型。
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 是正确时,我们知道该函数被正确应用并且我们可以提取结果。另一方面,如果 Either a 是左值,我们就知道出了问题,我们可以提取 theException 和原始值,所以我们可以继续进行。通过使用 Either 类型而不是将 check 包装 Exception 到 a RuntimeException 中,我们可以防止 Stream 中途终止。
3 尝试
可能使用过 Scala 的人可能会使用 Scala Try 而不是 Either 异常处理。类型与 类型 Try 非常相似 Either 。同样,它有两种情况:“成功”或“失败”。失败只能容纳类型 Exception,而成功可以容纳你想要的任何类型。因此,这 Try 只不过 Either 是左侧类型(失败)固定为 type 的特定实现 Exception。
public class Try<Exception, R> {private final Exception failure;private final R succes;public Try(Exception failure, R succes) {this.failure = failure;this.succes = succes;}}
有些人相信它更容易使用,但我认为因为我们只能 Exception 在失败部分保持自身,所以我们遇到了与第一部分中解释的相同的问题 Either 。我个人更喜欢这种类型的灵活性 Either 。无论如何,在这两种情况下,如果您使用 Try 或 Either,您就解决了异常处理的初始问题,并且不要让您的流因 RuntimeException.
the Either 和 the Try 都很容易自己实现。另一方面,您还可以查看可用的功能库。例如,VAVR(以前称为 Javaslang)确实具有可用的类型和辅助函数的实现。我建议您看一下它,因为它的内容远不止这两种类型。但是,您已经问自己一个问题,您是否希望这个大型库仅作为异常处理的依赖项,而您只需几行代码就可以自己实现它。
4 结论
当你想使用一个抛出 a 的方法时, 如果你想在 lambda 中调用它 checkedException,你必须做一些额外的事情。将其包装成一个 RuntimeException 可以使其工作的解决方案。try/catch 如果您更喜欢使用这种方法,我建议您创建一个简单的包装工具并重复使用它,这样您就不会被每次都打扰了。
如果你想拥有更多的控制权,你可以使用 Either or Try 类型来包装函数的结果,这样你就可以把它当作一条数据来处理。抛出 a 时流不会终止 RuntimeException ,您可以随意处理流中的数据。
评论