案例如下,在list中存在可能引发空指针的异常,当我需要使用stream流处理list时需要使用try/catch来捕获异常,当然你也可以对任何类型进行处理并捕获任何异常,这里只是举例了List
public class StreamHandle {
public static void main(String[] args) {
new StreamHandle().myFunction();
}
public void myFunction() {
List<Integer> list = Arrays.asList(1, 2, null, 4, 5, 6, 7, 8);
list.stream()
.map(value -> {
try {
return value + 1;
} catch (NullPointerException e) {
e.printStackTrace();
throw new NullPointerException();
}
})
.forEach(System.out::println);
}
}
当我们捕获到异常时可以打印异常信息或者直接throw抛出异常来终止stream流到操作。
2
3
java.lang.NullPointerException
将lambda代码块中的代码抽到方法:增加代码的可读性
public void myFunction() {
List<Integer> list = Arrays.asList(1, 2, null, 4, 5, 6, 7, 8);
list.stream()
.map(this::doSomeThing)
.forEach(System.out::println);
}
private int doSomeThing(int value) {
try {
return value + 1;
} catch (NullPointerException e) {
e.printStackTrace();
throw new NullPointerException();
}
}
现在你可以方法中捕获到异常并做一些需要的处理。
对于很多异常处理,我们会包装成运行时异常。
常用的异常继承关系图:
因为一些异常比如IO异常不是继承自RuntimeException,而是直接继承自Exception,对于继承自RuntimeException的异常我们当然可以直接抛出:
private <T> void doSomeThing(T value) {
throw new NullPointerException();
}
但是直接继承自Exception的异常我们就不能直接抛出而是需要用throws在方法头处理向上抛出:否则会编译不通过
private <T> void doSomeThing(T value) throws FileNotFoundException {
throw new FileNotFoundException();
}
不过还有另一种常用的解决方案就是将异常包装为运行时异常,这样就可以不增加方法头的处理直接抛出:
private <T> void doSomeThing(T value) {
FileNotFoundException e = new FileNotFoundException();
throw new RuntimeException(e);
}
综上所诉,如果你不想在方法调用链中写许多重复的throws,包装成运行时异常抛出是非常优雅的做法!
因为lambda中调用的方法可能会抛出各种异常,你如果每次都使用try/catch包装成RuntimeException会显得很麻烦,我们可以为要调用的可能抛出异常的方法做一个自定义方法接口:
@FunctionalInterface
public interface CheckedFunction<T,R> {
R apply(T t) throws Exception;
}
然后我们可以写一个通用的工具函数包装定义的检查函数,在这个函数中使用try/cat捕获异常并转换成RuntimeException运行时异常抛出(或者其他的uncheckedException)
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);
}
};
}
现在在stream流中我们可以对任何可能抛出异常的方法,使用自定义的检查函数来包裹处理了
public void myFunction() {
List<Integer> list = Arrays.asList(1, 2, null, 4, 5, 6, 7, 8);
list.stream()
.map(wrap(this::doSomeThing))
.forEach(System.out::println);
}
使用方法二虽然可以方便的包裹可能产生的异常,但是当异常抛出时stream流也会立刻终止,当异常出现时,如果你不想停止stream流并继续执行后面的流,就要引入Either了。
Either是函数式语言里很常见的类型,但是目前java还没有。和Optional类型相似,Either是对两种结果的一种包装。它可以是左值的(Left)或者是右值的(Right),也可以都不是。左值和右值都可以是任意类型的。举个例子,如果我们有个Either类型,它的值可以是一个String或者Integer, Either
根据这个原理,我们可以用Either来实现处理,把异常信息存到左值,正常处理的数据存放到右值。
实现Either类:
import org.apache.commons.lang3.tuple.Pair;
import java.util.Optional;
import java.util.function.Function;
/**
* 处理函数的包装器 处理stream流中可能产生的异常*
* @author hexuan*
* @param
* @param
*/
public class Either<L, R> {
/**
* 左-存储异常信息*
*/
private final L left;
/**
* 右-存储正常的CheckedFunction处理结果*
*/
private final R right;
private Either(L left, R right) {
this.left = left;
this.right = right;
}
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));
}
};
}
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);
}
};
}
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 <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();
}
@Override
public String toString() {
if (isLeft()) {
return "Left(" + left + ")";
}
return "Right(" + right + ")";
}
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;
}
}
我们先看这个函数:通过在Either里添加这个静态的lift方法,我们现在可以简单地“改进”一个抛出checkedException的函数,让它返回一个Either对象。如果我们现在回到最原始的问题上,我们现在拿到的是一个Either的流(Stream),而不是一个包含RuntimeException的不稳定的流(Stream)。
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);
}
};
}
调用方式:
public void myFunction() {
List<Integer> list = Arrays.asList(1, 2, null, 4, 5, 6, 7, 8);
list.stream()
.map(Either.lift(this::doSomeThing))
.forEach(System.out::println);
}
执行结果:现在正确数据都存放到了Right,而错误的数据则是Left,我们甚至可以根据需要分开处理了
Right(2)
Right(3)
Left(java.lang.NullPointerException)
Right(5)
Right(6)
Right(7)
Right(8)
Right(9)
比如可以对异常进行日志操作(可以封装为方法),正确返回的数据按需要处理:
public void myFunction() {
List<Integer> list = Arrays.asList(1, 2, null, 4, 5, 6, 7, 8);
list.stream()
.map(Either.lift(this::doSomeThing))
.peek(either -> {
if (either.getLeft().isPresent()) {
System.out.println(JSONObject.toJSONString(either.getLeft().get()));
}
})
.filter(either -> either.getRight().isPresent())
.forEach(either -> System.out.println(either.getRight().get()));
}
输出结果:
2
3
{"@type":"java.lang.NullPointerException","stackTrace":[{"className":".........
5
6
7
8
9
引入了Pari类,这是一个能保存两个值的类型,我上面是直接引入了现成的,你也可以自己定义这个能保存两个值的简单类型,它大概长这样:
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);
}
}
函数定中异常处理的部分就再用Pari包一层异常和原数据就可以啦:
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));
}
};
}
当你在使用一个抛出checkedException的函数式,如果你想要在lambda里使用它,你需要做一些额外的工作,比如将异常包装成RuntimeException是一种可行的方案。这种方法非常适合创建一个简单的工具包装函数,然后每次你只需要调用这个检查函数而不必再写try/catch。
如果你想进一步控制异常并在不终止stream的需求下,可以使用Either来再对函数包装,把异常和正确执行结果分开处理。