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 类型,那么你可以选择已有的工具库来使用

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐