Stream API 和 lambda 是 Java8以来对Java的重大改进。从那时起,我们可以使用更具有功能性的语法风格的代码。但是有个问题就是,我们使用了 lambda 表达式,那 lambda 中的异常该怎么处理呢。

大家都知道,不能直接在 lambda 中调用那些会抛出异常的方法,因为这样从编译上都通不过。所以我们需要捕获异常以使代码能够编译通过。

例如,我们可以在 lambda 中做一个简单的 try-catch 并将异常包装成一个 RuntimeException,如下面的代码所示,但这不是最好的方法。

myList.stream().map(t -> {try {return doSomething(t);} catch (MyException e) {throw new RuntimeException(e);}}).forEach(System.out::println);

我们大多数人都知道,lambda 代码块是笨重的,可读性较差。在我看来,应该尽可能避免直接在 lambda 中使用大量的代码段。

如果我们在 lambda 表达式中需要做多行代码,那么我们可以把这些代码提取到一个单独的方法中,并简单地调用新方法。

所以,解决此问题的更好和更易读的方法是将调用包装在一个普通的方法中,该方法执行 try-catch 并从 lambda 中调用该方法,如下面的代码所示:

myList.stream().map(this::trySomething).forEach(System.out::println);private T trySomething(T t) {try {return doSomething(t);} catch (MyException e) {throw new RuntimeException(e);}
}

这个解决方案至少有点可读性,并且将我们所关心的的问题也解决了。如果你真的想要捕获异常并做一些特定的事情而不是简单地将异常包装成一个 RuntimeException,那么这对你来说可能是一个还不错的解决方案。

一.包装成运行时异常

在许多情况下,你会看到大家都喜欢将异常包装成一个RuntimeException,或者是一个具体的未经检查的异常类。这样做的话,我们就可以在 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);}};
}

但是这种写法也是一个比较丑陋的 lambda 代码块,你可以选择要不要再对方法进行抽象。

通过简单的静态导入,你现在可以使用全新的通用方法来包装可能引发异常的lambda,如下列代码所示:

myList.stream().map(wrap(t -> doSomething(t))).forEach(System.out::println);

剩下的唯一问题是,当发生异常时,你的 stream 处理会立即停止。如果你的业务可以容忍这种情况的话,那没问题,但是,我可以想象,在许多情况下,直接终止并不是最好的处理方式。

二.包装成 Either 类型

使用 stream 时,如果发生异常,我们可能不希望停止处理。如果你的 stream 包含大量需要处理的项目,你是否希望在例如第二个项目引发异常时终止该 stream 呢?可能不是吧。

那我们可以换一种方式来思考,我们可以把 “异常情况” 下产生的结果,想象成一种特殊性的成功的结果。那我们可以把他们都看成是一种数据,不管成功还是失败,都继续处理流,然后决定如何处理它。我们可以这样做,这就是我们需要引入的一种新类型 - Either类型。

Either 类型是函数式语言中的常见类型,而不是 Java 的一部分。与 Java 中的 Optional 类型类似,一个 Either 是具有两种可能性的通用包装器。它既可以是左派也可以是右派,但绝不是两者兼而有之。左右两种都可以是任何类型。

例如,如果我们有一个 Either 值,那么这个值可以包含 String 类型或 Integer 类型:Either。

如果我们将此原则用于异常处理,我们可以说我们的 Either 类型包含一个 Exception 或一个成功的值。为了方便处理,通常左边是 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 +")";}}

你现在可以让你自己的函数返回 Either 而不是抛出一个 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 流而不是一个可能会终止我们的 Stream 的 RuntimeException,具体的代码如下:

myList.stream().map(Either.lift(item -> doSomething(item))).forEach(System.out::println);

通过在 Stream APU 中使用过滤器功能,我们可以简单地过滤出左侧实例,然后打印日志。也可以过滤右侧的实例,并且忽略掉异常的情况。无论哪种方式,你都可以对结果进行控制,并且当可能 RuntimeException 发生时你的流不会立即终止。

因为 Either 类型是一个通用的包装器,所以它可以用于任何类型,而不仅仅用于异常处理。这使我们有机会做更多的事情而不仅仅是将一个 Exception 包装到一个 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,你现在可以灵活的并且可控制的来在 lambda 表达式中调用可能会抛出 Exception 的方法了。

如果 Either 是一个 Right 类型,我们知道我们的方法已正确执行,我们可以正常的提取结果。另一方面,如果 Either 是一个 Left 类型,那意味着有地方出了问题,我们可以提取 Exception 和原始值,然后我们可以按照具体的业务来继续处理。

通过使用 Either 类型而不是将被检查包装 Exception 成 RuntimeException,我们可以防止 Stream 中途终止。

三.包装成 Try 类型

使用过 Scala 的人可能会使用 Try 而不是 Either 来处理异常。Try 类型与 Either 类型是非常相似的。

它也有两种情况:“成功”或“失败”。失败时只能保存 Exception 类型,而成功时可以保存任何你想要的类型。

所以 Try 可以说是 Either 的一种固定的实现,因为他的 Left 类型被确定为 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;}}

有人可能会觉得 Try 类型更加容易使用,但是因为 Try 只能将 Exception 保存在 Left 中,所以无法将原始数据保存起来,这就和最开始 Either 不使用 Pair 时遇到的问题一样了。所以我个人更喜欢 Either 这种更加灵活的。

无论如何,不管你使用 Try 还是 Either,这两种情况,你都解决了异常处理的初始问题,并且不要让你的流因为 RuntimeException而终止。

四.使用已有的工具库

无论是 Either 和 Try 是很容易实现自己。另一方面,您还可以查看可用的功能库。例如:VAVR(以前称为Javaslang)确实具有可用的类型和辅助函数的实现。我建议你去看看它,因为它比这两种类型还要多得多。

但是,你可以问自己这样一个问题:当你只需几行代码就可以自己实现它时,是否希望将这个大型库作为依赖项进行异常处理。

结论

当你想在 lambda 表达式中调用一个会抛出异常的方法时,你需要做一些额外的处理才行。

  • 将其包装成一个 RuntimeException 并且创建一个简单的包装工具来复用它,这样你就不需要每次都写try/catch 了

  • 如果你想要有更多的控制权,那你可以使用 Either 或者 Try 类型来包装方法执行的结果,这样你就可以将结果当成一段数据来处理了,并且当抛出 RuntimeException 时,你的流也不会终止。

  • 如果你不想自己封装 Either 或者 Try 类型,那么你可以选择已有的工具库来使用

技巧 | Java 8 Stream 中异常处理的4种方式相关推荐

  1. python中异常处理的两种方式_Python 之异常处理

    一 错误和异常 •错误分两种: 1.语法错误 1 #!/usr/bin/env python 2 #-*- coding:utf-8 -*- 3 #举列 4 print('hello world' # ...

  2. java 8 stream中的Spliterator简介

    文章目录 简介 tryAdvance trySplit estimateSize characteristics 举个例子 总结 java 8 stream中的Spliterator简介 简介 Spl ...

  3. java 8 Stream中操作类型和peek的使用

    文章目录 简介 中间操作和终止操作 peek 结论 java 8 Stream中操作类型和peek的使用 简介 java 8 stream作为流式操作有两种操作类型,中间操作和终止操作.这两种有什么区 ...

  4. 【Java8新特性】面试官问我:Java8中创建Stream流有哪几种方式?

    写在前面 先说点题外话:不少读者工作几年后,仍然在使用Java7之前版本的方法,对于Java8版本的新特性,甚至是Java7的新特性几乎没有接触过.真心想对这些读者说:你真的需要了解下Java8甚至以 ...

  5. 12月18日云栖精选夜读 | Java 中创建对象的 5 种方式!...

    作为Java开发者,我们每天创建很多对象,但我们通常使用依赖管理系统,比如Spring去创建对象.然而这里有很多创建对象的方法,我们会在这篇文章中学到. Java中有5种创建对象的方式,下面给出它们的 ...

  6. Java中创建对象的几种方式

    Java中创建对象的几种方式 1.使用new创建对象,在堆上创建. 2.克隆 3.反序列化 4.反射创建对象 5.NIO中可以使用本地方法直接分配堆外内存. 转载于:https://www.cnblo ...

  7. Java中创建对象的四种方式

    为什么80%的码农都做不了架构师?>>>    Java中创建对象的四种方式 (1) 用new语句创建对象,这是最常见的创建对象的方法.    (2) 运用反射手段,调用java.l ...

  8. java之pdf转图片的几种方式,以及在使用过程中遇到的问题和处理方案

    java之pdf转图片的几种方式,以及在使用过程中遇到的问题和处理方案 方式 pdfBox iText(方式同上) spire.pdf.free(方式同上) 遇到的问题 图片清晰度.这种方式都有这个问 ...

  9. try_catch_异常处理的第二种方式,自己处理异常

    package com.learn.demo02.Exception;import java.io.IOException;/*try...catch:异常处理的第二种方式,自己处理异常格式:try{ ...

最新文章

  1. django正反向查询
  2. python xlrd处理表格常用方法
  3. 谈一谈:抽象工厂+反射+配置文件 实现数据访问程序
  4. oss可用性_对象存储OSS详解
  5. cmake导入so库_libgo - 协程库、并行编程库
  6. 怎样才能让Android平板不卡,如何让你的安卓平板从获新生
  7. 使用请求头认证来测试需要授权的 API 接口
  8. java底层 文件操作,java底层是怎的对文件操作的
  9. 微积分中BW定理指的是什么
  10. 《数据挖掘概念与技术》读书笔记(一)
  11. Halcon的一维码和二维码解码步骤和技巧
  12. 华为手机进工程模式指令大全
  13. 使用AVPlayer+AFNetworking封装一个带有缓存逻辑的音频播放器
  14. 智能网联汽车云控系统第5部分:平台服务场景规范
  15. java中强制退出jvm的代码
  16. The server time zone value is unrecognized or represents more than one time zone. 这个问题的解决方法
  17. 制作篇3 - 制作agent-server镜像包
  18. 项目前期准备的重要性
  19. 《机器学习实战》斧头书——第三章—决策树(1)——使用决策树预测你是否需要带隐形眼镜
  20. ARUBA无线控制器内置网管功能介绍

热门文章

  1. python中文件的读取与写入以及os模块
  2. CSS三栏自适应布局,左中右,上中下
  3. yum 方式安装nginx
  4. Nagios设置报警间隔
  5. js基础语法知识(数组/对象/日期)
  6. timestamp类型设置默认时间
  7. ThreadLocal_OSIV模式_FIlter_Web ajax
  8. 信管师培训之第十二节课作业(外包管理+需求管理+组织级与大项目管理)
  9. jQuery学习笔记(四)——表单选择
  10. wamp配置多少站点