前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >技巧 | Java 8 Stream 中异常处理的4种方式

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

作者头像
用户1516716
发布于 2019-05-17 08:29:04
发布于 2019-05-17 08:29:04
7.7K00
代码可运行
举报
文章被收录于专栏:A周立SpringCloudA周立SpringCloud
运行总次数:0
代码可运行

点击上方 IT牧场 ,选择 置顶或者星标技术干货每日送达!>>>技术讨论群<<<

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 中调用该方法,如下面的代码所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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中,那你会看到很多重复的代码。为了避免一遍又一遍地重写相同的代码,我们可以将它抽象为一个方法,这样,你只需编写一次然后每次需要的时候直接调用他就可以了。

首先,你需要为函数编写自己的方法接口。只有这一次,你需要定义该函数可能抛出异常,例如下列所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@FunctionalInterfacepublic interface CheckedFunction<T,R> {    R apply(T t) throws Exception;}

现在,您可以编写自己的通用方法了,它将接受一个 CheckedFunction 参数。你可以在这个通用方法中处理 try-catch 并将原始异常包装到 RuntimeException中,如下列代码所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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,如下列代码所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 类型,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 的话,你还需要对原有的代码做一些调整,如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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,具体的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
myList.stream()       .map(Either.lift(item -> doSomething(item)))       .forEach(System.out::println);

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

因为 Either 类型是一个通用的包装器,所以它可以用于任何类型,而不仅仅用于异常处理。这使我们有机会做更多的事情而不仅仅是将一个 Exception 包装到一个 Either 的左侧实例中。

我们现在可能遇到的问题是,如果 Either 只保存了包装的异常,并且我们无法重试,因为我们丢失了原始值。

通过使用 Either 保存任何东西的能力,我们可以将异常和原始值都保存在左侧。为此,我们只需制作第二个静态提升功能。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 库中找到,或者你也可以简单地实现自己的类型。

无论如何,它只是一个可以容纳两个值的类型,如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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了,如下列的代码所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 类型,那么你可以选择已有的工具库来使用

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-05-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 IT牧场 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
SpringBoot自定义注解实现Token校验
来自:blog.csdn.net/qq_33556185/article/details/105420205
良月柒
2021/03/09
5340
HandlerMethodArgumentResolver(三):基于HttpMessageConverter消息转换器的参数处理器【享学Spring MVC】
通过 前面两篇文章 的介绍,相信你对HandlerMethodArgumentResolver了解已经很深刻了。但是你或许和我一样还有一种感觉,似乎还缺点什么: 我们使用非常频繁的@RequestBody是怎么封装请求体的呢???这块使用非常广泛的地方却还木有讲解到,因为它的处理方式和前面的不太一样,因此单摘出来到本文进行详细描述。
YourBatman
2019/09/03
1.5K0
HandlerMethodArgumentResolver(三):基于HttpMessageConverter消息转换器的参数处理器【享学Spring MVC】
SpringMvc源码之解析参数HandlerMethodArgumentResolver
HandlerMethodArgumentResolver是什么?它是springmvc提供的入参解析器,像平常应用的注解@RequestParam @PathVariable @ModelAttribute ...等等修饰在@RequestMapping下的参数上都可以用HandlerMethodArgumentResolver来解析。
简单的程序员
2020/04/18
9430
HandlerMethodArgumentResolver(二):Map参数类型和固定参数类型【享学Spring MVC】
上文 介绍了Spring MVC用于处理入参的处理器:HandlerMethodReturnValueHandler它的作用,以及介绍了最为常用的两个参数处理器子类:PathVariableMethodArgumentResolver和RequestParamMethodArgumentResolver。由于该体系的重要以及庞大,本文将接着继续讲解~
YourBatman
2019/09/03
1.7K0
HandlerMethodArgumentResolver(二):Map参数类型和固定参数类型【享学Spring MVC】
【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---一篇文章带你读懂返回值处理器HandlerMethodReturnValueHandler
Spring MVC处理入参靠的是HandlerMethodArgumentResolver这个接口,解析返回值靠的是HandlerMethodReturnValueHandler这个策略接口。
YourBatman
2019/09/03
1.4K0
【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---一篇文章带你读懂返回值处理器HandlerMethodReturnValueHandler
springboot源码解析之Model和Map参数解析
通过输出可以看到,这里传入的Model对象和Map对象都是BindingAwareModelMap类型,并且指向同一个对象,BindingAwareModelMap既是Model也是Map
九转成圣
2024/04/10
1570
springboot源码解析之Model和Map参数解析
记一次springboot项目自定义HandlerMethodArgumentResolver不生效原因与解法
本文素材的来源自业务部门技术负责人一次代码走查引发的故事,技术负责人在某次走查成员的代码时,发现他们的业务控制层大量充斥着如下的代码
lyb-geek
2022/10/25
1.1K0
记一次springboot项目自定义HandlerMethodArgumentResolver不生效原因与解法
Spring MVC请求处理过程。你这样回答保证通过面试!
SpringMVC请求处理相信大家都很熟悉了,本篇主要是基于SpringMVC处理请求的流程来阅读并调试源码,以及解决几个仅靠流程图无法解释的问题。
程序员白楠楠
2020/12/07
7430
教你如何通过 ArgumentResolver 与 Filter 优化你的 SpringMVC 设计
Spring 提供了一个非常强大的工具来让你能够为若干 controller 封装通用参数的实例化逻辑,这样,你的 controller 就无需去做那些重复的参数实例化工作,从而让你的 controller 变得更为简洁。这个机制就是本文要介绍的 MethodArgumentResolver。
用户3147702
2022/06/27
4850
HandlerMethodArgumentResolver :深入spring mvc参数解析机制
HandlerMethodArgumentResolver 是 Spring MVC 框架中的一个关键组件,用于解析控制器(Controller)方法的参数。在 Spring MVC 中,当一个请求到达时,DispatcherServlet 会负责找到对应的处理器(即控制器中的方法)来处理这个请求。在处理之前,需要解析方法的参数,这就是 HandlerMethodArgumentResolver 的作用
公众号:码到三十五
2024/07/15
8580
HandlerMethodArgumentResolver :深入spring mvc参数解析机制
你真的了解Spring MVC处理请求流程吗?
前言 阅读本文章大概需要8分钟左右。相信会让你对Spring MVC的理解更加深刻,更上一层楼。 SpringMVC图解 粒度很粗的图解 自己画的.png
用户2032165
2018/07/02
2K0
spring 之 spring-mvc
spring-mvc的核心便是DispatcherServlet,所以初始化也是围绕其展开的。类图:
MickyInvQ
2021/10/22
1.1K0
spring 之 spring-mvc
万字长文,深度解析SpringMVC 源码,让你醍醐灌顶!!
本文将通过阅读源码的方式带大家了解 springmvc 处理请求的完整流程,干货满满。
路人甲Java
2021/10/08
2.4K0
参数解析-HandlerMethodArgumentResolver
今天在做项目时遇到了一个有关参数解析 HandlerMethodArgumentResolver 的使用疑惑。因此去 百度学习了一下,现在记录一下。
Blue_007
2023/11/03
2810
参数解析-HandlerMethodArgumentResolver
第三十六章:基于SpringBoot架构重写SpringMVC请求参数装载
在国内企业开发项目中大多数都已经偏向Spring家族式的开发风格,在前几年国内项目都是以Structs2作为Web开发的主导,不过由于近几年发生的事情确实让开发者对它失去了以往的信心。与此同时Spring家族发布了SpringMVC,而且完美的整合Spring来开发企业级大型Web项目。它有着比Structs2更强大的技术支持以及更灵活的自定义配置,接下来我们就看看本章的内容,我们自定义实现SpringMVC参数绑定规则,根据业务定制参数装载实现方式。 本章目标 根据项目定制SpringMVC参数状态并了解
恒宇少年
2018/06/27
1.4K0
spring mvc之HandlerMethodArgumentResolver
因为:1.加重了我们对请求传过来来的值的取值代码,会使控制器中request.getParamater()之类的代码越来越多;2.不利于测试;3.request.getParamater()只能获取string,如果是Long等其他类型的参数还需要强转,使用起来非常不方便。
BUG弄潮儿
2022/06/30
2710
SpringBoot2核心技术-web开发
ServletModelAttributeMethodProcessor 这个参数处理器支持
yuanshuai
2022/08/22
7800
SpringBoot2核心技术-web开发
如何妙用Spring 数据绑定机制
在剖析完 Spring Boot 返回统一数据格式是怎样实现的?文章之后,一直觉得有必要说明一下 Spring's Data Binding Mechanism 「Spring 数据绑定机制」。
用户4172423
2019/12/25
1.2K0
如何妙用Spring 数据绑定机制
Spring自定义参数解析器设计
我们在开发Controller接口时经常会用到此类参数注解,那这些注解的作用是什么?我们真的了解吗?
程序猿川子
2023/05/04
6470
面试题之--SpringMVC 原理
一次为了解决跨域问题,采用了CORS方法。根据官方解释,只需要在响应头里设置 1、Access-Control-Allow-Origin 2、Access-Control-Allow-Methods 3、Access-Control-Allow-Headers 三个值就可以了,于是想到在HandlerInterceptor#preHandle()里去拦截跨域请求(options),然后再根据自定义注解判断请求的controller是否支持跨域请求,再设置对应的响应头。(项目基于spring3.2.x)但是发现请求死活无法进入preHandle里(项目里只有一个自定义的preHandle,不存在提前被别的HandlerInterceptor返回的情况)。于是利用debug大法,发现spring获取拦截器时是根据url和请求类型进行判断的,由于跨域请类型是options,无法获取对于的handler和HandlerInterceptor,导致直接就返回了,没有进入拦截器里。(spring4.x后有个默认的handler支持处理options)。于是把debug过程中学习到的知识,下次排查问题可以更快。
公众号 云舒编程
2024/01/25
1320
面试题之--SpringMVC 原理
推荐阅读
相关推荐
SpringBoot自定义注解实现Token校验
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档