技巧Java8Stream中异常处理的4种⽅式
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) {
java streamtry {
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));
}
pty();
}
public <T> Optional<T> mapRight(Function<? super R, T> mapper) {
if (isRight()) {
return Optional.of(mapper.apply(right));
}
pty();
}
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 类型,那么你可以选择已有的⼯具库来使⽤

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。