简介
在实现代码的过程中,使用一些函数接口可以使得代码变得更加简洁。本文基于Scala的几个比较使用的函数接口翻译成Java版本来进行一个简单的总结。
如果只是想使用的话,推荐使用现成的包来提高稳定性:
1
2
3
4
5
6<!-- https://mvnrepository.com/artifact/io.vavr/vavr -->
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.3</version>
</dependency>以下的内容是自己实现的这些接口,以方便来扩展一些功能,仅供参考。
Option
基本简介
- Option(选项)类型用来表示一个值是可选的(有值或无值)。
- Option[T] 是一个类型为 T 的可选值的容器: 如果值存在, Option[T] 就是一个 Some[T] ,如果不存在, Option[T] 就是对象 None。
- 在Java中,Option给了一个实现是Java中的Optional,其可以表示一个可空的对象,以此来防止空指针的bug。
- 使用场景:
- 用于标识一个方法的返回值是可能为空的,让外部人员知道这个值可能为空,记得进行空场景的处理。
- 在存在多层if判空的情况下,可以使用Option(Optional)来减少if的层级。具体可以参考本文的上一篇文章。
Lazy
基本简介
- 使用Lazy来定义惰性变量,实现延迟加载(懒加载)。惰性变量只能是不可变变量,并且只有在调用惰性变量时,才会去实例化这个变量。
代码实现
- 本实现中,提供了一个Lazy及其子类(ParallelLazy),其分别使用
of
方法和ofParallel
方法来进行创建。后者使用了Double-checked locking来防止了多线程使用此类的并发问题(实际上延迟加载多线程的场景还是比较多的)。- 并发场景下,Lazy如果不额外做并发的处理的话,会里面的方法重复执行。
1 |
|
实际场景
存在以下场景:在一段比较长的代码中,一个变量可能在一些条件分支里不一定被使用,但可能有多个条件分支(甚至以后可能还会加入新的条件分支)使用此变量。那么有两种选择:在最外层定义,或者在if里面定义。前者会导致在每个if分支里面额外做空判断(或者一开始就赋值,但是如果后续的代码没用到这个变量,那么就是个无用的操作);后者会导致一个变量在if里面多次赋值,降低代码的可读性。
如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16Student student = null;
if(/*条件 1*/){
if(student == null){
student = getStudent(id);
}
/*使用student*/
}
/*其他代码*/
if(/*条件 2*/){
if(student == null){
student = getStudent(id);
}
/*使用student*/
}可以看到,这里在不同的条件分支里面,为了使用Student多次对其进行了判空(我们假定
getStudent
这个方法永远不会返回空),导致了if的层级加深。如果使用Lazy来替代的话,就可以将上述代码替换为如下代码:
1
2
3
4
5
6
7
8
9Lazy<Student> studentLazy = Lazy.of(() -> getStudent(id));
if(/*条件 1*/){
String name = studentLazy.get().getName();
/*使用student*/
}
if(/*条件 2*/){
Boolean sex = studentLazy.get().getSex();
/*使用student*/
}可以发现,代码实际上简洁了不少,可能在这段demo代码里面看不出来有太大的用处,但是在一些老项目的代码中,减少一些额外的代码对阅读代码还是很有帮助的。
Either
基本简介
- Either 也是一个容器类型,但不同于 Try、Option,它需要两个类型参数:
Either[A, B]
要么包含一个类型为A
的实例,要么包含一个类型为B
的实例。 - Either 只有两个子类型: Left、 Right, 如果
Either[A, B]
对象包含的是A
的实例,那它就是 Left 实例,否则就是 Right 实例。 - 在语义上,Either 并没有指定哪个子类型代表错误,哪个代表成功, 毕竟,它是一种通用的类型,适用于可能会出现两种结果的场景。 而异常处理只不过是其一种常见的使用场景而已, 不过,按照约定,处理异常时,Left 代表出错的情况,Right 代表成功的情况。
代码实现
1 |
|
1 |
|
实际场景
一种场景是作为Service层方法的返回值:如一个登录方法,登录成功后返回用户信息,我们在Service层处理的过程中,可能会遇到找不到用户、用户密码不匹配的情况。我们可以通过异常来处理这些错误情况。如下:
1
2
3
4
5
6
7
8
9
10public User login(String account, String password){
User user = userDao.findUserByAccount(account);
if(user != null){
throw new ServiceException("用户不存在");
}
if(Objects.equals(password, user.getPassword())){
throw new ServiceException("用户密码错误");
}
return user;
}这种场景,如果是在本地接口的话,那么其实这种方法挺合适的,但是如果这个是一个对外的RPC方法,那么这个就不合理了。外层的方法不应该去处理你这些业务异常。那么,我们就可以使用Either来进行修改:
1
2
3
4
5
6
7
8
9
10public Either<String, User> login(String account, String password){
User user = userDao.findUserByAccount(account);
if(user != null){
return Either.left("用户不存在");
}
if(Objects.equals(password, user.getPassword())){
return Either.left("用户密码错误");
}
return Either.right(user);
}这样RPC调用方,就可以通过Either来判断调用结果是否成功了,甚至能通过map方法进行链式的后续处理。
另外一种场景就是可以将其当作下面的Try来使用。例如读取Excel这种场景,使用Either来防止Excel格式不正确就很方便了。
Try
基本简介
- Try 有两个子类型:
Success[A]
:代表成功的计算。- 封装了
Throwable
的Failure[A]
:代表出了错的计算。
- 如果知道一个计算可能导致错误,我们可以简单的使用 Try[A] 作为函数的返回类型。 这使得出错的可能性变得很明确,而且强制客户端以某种方式处理出错的可能。
- Try的使用和Option比较类似,同时,Try可以相当于一个特化版的Either。
代码实现
- 这里除了实现了Try的功能,还给Try封装了一个sneakyThrows的语义,这个和Lombok中的SneakyThrows就比较类似了。
1 |
|
- 一些Functional接口的实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52/**
* 受检查的Action,,其可以抛出异常
*
* @author Gloduck
* @date 2022/12/26
*/
@FunctionalInterface
public interface CheckedAction {
/**
* 执行
* @throws Throwable 可能的异常
*/
void action() throws Throwable;
}
/**
* 受检查的Function,区别于内置的Function,其可以抛出异常
*
* @author Gloduck
* @date 2022/12/26
*/
@FunctionalInterface
public interface CheckedFunction<T, R> {
/**
* 使用给的参数获取值
* @param t 给的参数
* @return 获取的值
* @throws Throwable 可能的异常
*/
R apply(T t) throws Throwable;
}
/**
* 受检查的Supplier,区别于内置的Supplier,其可以抛出异常
*
* @author Gloduck
* @date 2022/12/26
*/
@FunctionalInterface
public interface CheckedSupplier<T> {
/**
* 获取值
*
* @return 获取的值
* @throws Throwable 可能的异常
*/
T get() throws Throwable;
}
使用场景
Try场景
Try其实就是相当于把
try...catch...
换了一种表示形式,这种形式在Lambda表达式中尤为可靠。如以下场景:有一批URI列表(String类型),里面可能包含不正确的URI,我们需要请求此URI获取其内容,那么不用Try的写法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13List<String> contentLists = urls.stream().map(s -> {
try {
return new URI(s);
} catch (URISyntaxException e) {
return null;
}
}).filter(Objects::nonNull).map(uri -> {
try {
return Files.readString(Path.of(uri));
} catch (IOException e) {
return "";
}
}).filter(Objects::nonNull).collect(Collectors.toList());可以看到上述代码为了解决异常,使得代码变得比较臃肿。替换为Try后,效果如下:
1
2
3
4
5List<Try<String>> contentLists = urls.stream()
.map(s -> Try.of(() -> new URI(s)))
.filter(Try::isSuccess)
.map(uriTry -> uriTry.thenTry(uri -> Files.readString(Path.of(uri))).repair(() -> ""))
.collect(Collectors.toList());
SneakyThrows场景
至于封装的SneakyThrows的使用如下,如当前线程睡眠几秒:
1
2
3
4
5try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}可以替换为:
1
Try.sneakyThrows(() -> TimeUnit.SECONDS.sleep(1));