当前位置: 首页 > 后端技术 > Java

Java培训:JavaStreams中的异常处理方法分享

时间:2023-04-01 22:07:08 Java

以下文章来自ITCoders从8版本开始,StreamAPI和lambdas是Java的一大进步。从此,我们可以使用更函数式的语法风格。现在,在使用这些代码结构几年之后,仍然存在的一个更大的问题是如何在lambda中处理已检查的异常。您可能知道,无法直接调用从lambda抛出已检查异常的方法。在某些时候,我们需要捕获异常以使代码编译。当然我们可以在lambda中做一个简单的try-catch并将异常包装到RuntimeException中,如第一个示例所示,但我认为我们都同意这不是最好的方法。myList.stream().map(item->{try{returndoSomething(item);}catch(MyExceptione){thrownewRuntimeException(e);}}).forEach(System.out::println);大多数人都知道blocklambda很麻烦而且可读性差。在我看来,应该尽可能避免它们。如果我们需要做不止一行,我们可以将函数体提取到一个单独的方法中,并简单地调用新方法。解决此问题的更好且更易读的方法是将调用包装在一个普通的旧方法中,该方法执行try-catch并从您的lambda调用该方法。myList.stream().map(this::trySomething).forEach(System.out::println);privateItemtrySomething(Itemitem){try{returndoSomething(item);}catch(MyExceptione){thrownewRuntimeException(e);}}这个解决方案至少更具可读性,我们确实将我们的关注点分开了。如果您真的想捕获异常并做一些特定的事情而不是简单地将异常包装在RuntimeException中,那么这对您来说可能是一个可行且可读的解决方案。1RuntimeExceptions在很多情况下你会看到人们使用这些类型的解决方案将异常重新包装成一个或多个特定的RuntimeExceptionuncheckedexception的实现。通过这样做,可以在lambda内部调用该方法并在高阶函数中使用。我可以将此与这种做法联系起来,因为我个人认为一般情况下检查异常没有太大价值,但这是另一个完整的讨论,我不打算在这里开始。如果您想将每个调用包装在检查RuntimeException的lambda中,您会看到您重复相同的模式。为了避免一遍又一遍地重写相同的代码,为什么不把它抽象成一个实用函数呢?这样,您只需编写一次,每次需要_java_training时调用它。为此,您首先需要为函数编写自己版本的功能接口。只有这一次,您需要定义该函数可能会抛出异常。@FunctionalInterfacepublicinterfaceCheckedFunction{Rapply(Tt)throwsException;}现在您已准备好编写您自己的通用实用函数,它接受您刚刚在界面中描述的CheckedFunction。您可以在此实用程序函数中处理try-catch,并将原始异常包装到RuntimeException(或其他一些未经检查的变体)中。我知道我们现在在这里得到一个丑陋的块lambda,您可以从中抽象出主体。自己选择是否值得为这个单一的实用程序付出努力。publicstaticFunctionwrap(CheckedFunctioncheckedFunction){returnt->{try{returncheckedFunction.apply(t);}catch(Exceptione){抛出新的RuntimeException(e);}};}通过简单的静态导入,您现在可以使用全新的实用函数包装可能引发异常的lambda。从这一点开始,一切重新开始。myList.stream().map(wrap(item->doSomething(item))).forEach(System.out::println);唯一剩下的问题是当发生异常时流的处理立即停止。如果这对你来说没问题,那就去吧。但是,我可以想象在很多情况下直接终止并不理想。2要么在处理流的过程中,如果发生异常,我们可能不想停止处理流。如果您有一个包含很多项目要处理的流,您是否希望在第二个项目抛出异常时终止流?也许不会。让我们换个思路。为什么不像我们考虑“成功”结果一样,将“特殊情况”视为可能的结果。让我们将其视为数据,继续处理流,并决定如何处理它。我们可以做到这一点,但要使其成为可能,我们需要引入一种新类型-Either类型。Either类型是函数式语言中的常见类型,(目前)还不是Java的一部分。类似于Java中的Optional类型,Either是具有两种可能性的通用包装器。它可以向左或向右,但不能同时向左。left和right都可以是任何类型。例如,如果我们有一个Either值,该值可以包含String类型或Integer类型Either。如果我们使用这个原则来处理异常,我们可以说我们的Either类型要么持有一个值,要么Exception持有一个值。为了方便,通常离群值在左边,成功值在右边。记住这一点,不仅要使右手边变右,还要使“好”、“好”等的同义词变右。下面,您将看到类型Either的基本实现。在这种情况下,Optional当我们尝试取左或右时,我使用了该类型,因为我们:publicclassEither{privatefinalLleft;privatefinalRright;privateEither(L左,R右){this.left=left;this.right=right;}publicstaticEitherLeft(Lvalue){returnnewEither(value,null);}publicstaticEitherRight(R值){returnnewEither(null,value);}publicOptionalgetLeft(){returnOptional.ofNullable(left);}publicOptionalgetRight(){returnOptional.ofNullable(right);}publicbooleanisLeft(){returnleft!=null;}publicbooleanisRight(){returnright!=null;}publicOptionalmapLeft(Functionmapper){if(isLeft()){returnOptional.of(mapper.apply(left));}returnOptional.empty();}publicOptionalmapRight(Functionmapper){if(isRight()){returnOptional.of(mapper.apply(right));}returnOptional.empty();}publicStringtoString(){if(isLeft()){return"Left("+left+")";}return"Right("+right+")";}}你现在可以让你的函数返回一个Either而不是抛出异常但是如果您想使用在lambda中进行检查的现有方法,这对您没有帮助吗?所以我们必须向上面描述的Either类型添加一个微小的实用函数publicstaticFunctionlift(CheckedFunctionfunction){returnt->{try{returnEither.Right(function.apply(t));}catch(Exceptionex){returnEither.Left(ex);}};}通过将这个静态提升方法添加到Either。如果我们考虑原来的问题,我们现在会得到一个Eithers流,而不是一个可能的RuntimeException,它会炸毁我的整个Stream.myList.stream().map(Either.lift(item->doSomething(item))).forEach(System.out::println);这只是意味着我们已经收回了控制权。通过使用StreamAPU中的过滤器功能,我们可以简单地过滤掉左侧的实例,例如记录它们。您还可以过滤正确的实例并简单地忽略异常。RuntimeException无论哪种方式,您都将重新获得控制权,并且您的流不会在可能发生时立即终止。因为Either是通用包装器,所以它可以用于任何类型,而不仅仅是用于异常处理。这使我们有机会做更多的事情,而不仅仅是将Exception包装到Either中。我们现在可能遇到的问题是,如果Either是唯一持有包装异常的东西,我们就不能重试,因为我们失去了原来的价值。通过使用Either的能力来保存任何东西,我们可以在left中存储异常和值。为此,我们只需像这样制作第二个静态提升功能。publicstaticFunctionliftWithValue(CheckedFunctionfunction){returnt->{try{returnEither.Right(function.apply(t));}catch(异常ex){returnEither.Left(Pair.of(ex,t));}};}可以看到,在这个liftWithValue函数中,Pair类型用于异常和原始值对Either的配对。现在左侧,如果出现问题,我们拥有我们可能需要的所有信息,而不仅仅是异常。这里使用的Pair类型是另一种通用类型,可以在ApacheCommons语言库中找到,或者您可以简单地实现自己的类型。无论如何,它只是一种可以容纳两个值的类型。publicclassPair{publicfinalFfst;publicfinalSsnd;privatePair(Ffst,Ssnd){this.fst=fst;this.snd=snd;}publicstaticof(Ffst,Ssnd){returnnewPair<>(fst,snd);}}通过使用liftWithValue,您现在拥有所有的灵活性和控制权来使用lambda方法中可能抛出的异常。当Either为真时,我们知道该函数已正确应用并且我们可以提取结果。另一方面,如果Eithera是一个左值,我们就知道出了问题,我们可以提取theException和原始值,这样我们就可以继续了。通过使用Either类型而不是将检查异常包装到RuntimeException中,我们可以防止Stream中途终止。3Try可能使用过Scala的人可能会使用ScalaTry而不是Either异常处理。Try类型与Either类型非常相似。同样,它有两种情况:“成功”或“失败”。Failure只能持有Exception类型,而success可以持有任何你想要的类型。所以这个Try只不过是Either的一个具体实现,它的左类型(失败)固定为类型Exception。publicclassTry{privatefinalExceptionfailure;privatefinalRsucces;publicTry(Exceptionfailure,Rsucces){this.failure=failure;this.succes=succes;}}有些人认为它更容易使用,但我认为由于我们只能将Exception本身保留在失败部分,所以我们遇到了与Either的第一部分中解释的相同的问题。我个人更喜欢Either类型的灵活性。不管怎样,在这两种情况下,如果你使用Try或Either,你就解决了原来的异常处理问题,并且不会让你的流导致RuntimeException。Either和Try很容易自己实现。另一方面,您也可以查看可用的函数库。例如,VAVR(以前称为Javaslang)确实具有可用的类型和辅助函数的实现。我建议你看看它,因为它远不止这两种类型。但是,您已经问过自己一个问题,您是否希望这个大型库仅作为异常处理的依赖项,您只需几行代码即可自行实现。4结论当你想使用一个抛出checkedException的方法时,如果你想在lambda中调用它,你必须做一些额外的事情。将其包装在RuntimeException中将使解决方案起作用。如果您更喜欢使用try/catch,我建议您创建一个简单的包装器并重新使用它,这样您就不会每次都被卡住。如果你想有更多的控制权,你可以使用Either或Try类型来包装函数的结果,这样你就可以把它当作一条数据来对待。抛出RuntimeException时流不会终止,您可以自由地对流中的数据执行任何操作。

猜你喜欢