JDK 8 函数式编程最佳实践
Posted dm_vincent
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK 8 函数式编程最佳实践相关的知识,希望对你有一定的参考价值。
文章导航
JDK 8 函数式编程最佳实践
1. Lambda表达式的重要接口
1.1 新增的函数接口
主要指的是java.util.function包中的函数接口。
函数式接口:只含一个方法定义的接口。函数式接口能够定义一个Lambda表达式的类型。Lambda表达式实际是将特定形式的入参和返回值进行了抽象。
主要的抽象类型:
- Consumer:有一个入参,没有返回值
- Supplier:没有入参,有返回值
- Function:有一个入参,也有返回值
由此衍生来的常用抽象类型:
- Predicate:有一个入参,返回值为Boolean
- BiConsumer:有两个入参,没有返回值
- BiFunction:有两个入参,有返回值
- UnaryOperator:Function的特殊情况,入参和返回值类型相同
- BinaryOperator:BiFunction的特殊情况,两个入参和返回值的类型都相同
- Double,Integer以及Long的类型为primitive的各种特殊形式
根据这个定义,JDK中原来含有一个方法定义的接口都能够被上述的函数式接口进行抽象。
1.1 无参数,无返回值类型接口
典型的比如Runnable接口,定义一个Runnable实例不再需要使用匿名类的方式,直接使用Lambda表达式:
Runnable runnable = () ->
System.out.println("runnable");
;
实际上任何满足无入参,无返回值的方法都可以被引用成为一个Lambda表达式,然后赋值给一个Runnable实例:
Runnable testStatic = Test::testStaticRunnable;
private static void testStaticRunnable()
System.out.println("runnable in static");
1.2 无参数,有返回值类型接口
典型的比如Callable接口,它的定义实际上是一个Supplier类型。
1.3 有参数,也有返回值的类型接口
比如Comparator接口,它可以被一个ToIntBiFunction<T, T>抽象。所以对集合进行排序的操作可以这样写:
Collections.sort(list, (elem1, elem2) ->
return elem1.compareTo(elem2);
);
2. 方法引用
几种形式的引用列举如下。
2.1 静态方法引用
private static void testStaticRunnable()
System.out.println("runnable in static");
// execute方法接受一个Runnable实例作为参数,Runnable接口的定义符合testStaticRunnable方法的定义形式,因此可以将该方法引用成Lambda表达式传入到execute方法中
threadPool.execute(Test::testStaticRunnable);
2.2 实例方法引用
引用实例方法的规则:实例本身会作为第一个参数,实例方法接受的参数作为后续参数,然后返回值作为最后一个参数,就像下面这样:
BiFunction endsWith = String::endsWith;
endsWith方法接受一个String作为参数,返回一个Boolean作为返回值,因此被定义为一个BiFunction类型。
2.3 构造方法引用
构造方法本质上也是一个接受指定类型参数(或者不接受任何参数)的方法,只不过返回值是该类型的一个实例,因此可以这样来引用:
// 抽象为接受int作为参数,返回Sample类型的一个Function Lambda表达式
IntFunction sampleNew = Sample::new;
Sample sample = sampleNew.apply(5);
// 类定义
private class Sample
private int count;
Sample(int count)
this.count = count;
3. 使用Lambda表达式完成集合的规约操作
规约操作分为下面两种,通常的操作都可以通过Collectors这个工具类完成。
3.1 成为一个单值类型
比如:
- Collectors.joining,用来做字符串拼接
- Collectors.counting,用来统计集合数量
3.2 成为一个集合类型
比如常用的:
- Collectors.toList,将Stream转换成一个集合类型,集合实现是ArrayList,如果不想使用ArrayList的话,可以使用Collectors.toCollection
- Collectors.partitioningBy,接受一个Predicate类型的表达式,将一个集合按True/False作为键值分割成两个集合
- Collectors.groupingBy,partitioningBy的加强版,可以自定义Key
- Collectors.toMap,终极版本,需要接受一个keyMapper和valueMapper作为参数,它们都是Function类型的抽象形式
更多的Collector实现可以参看Collectors工具类。
3.3 stream和parallelStream
分别代表了串行执行模式和并行执行模式。当使用并行执行模式时,底层使用的是JDK7中引入的Forn/Join Framework。
在我们的代码中使用parallelStream时并不会提示PMD问题。使用的时候注意一个点:ThreadLocal不要在parallelStream的执行逻辑中使用,因为执行线程并不是当前线程。
另外,由于ParallelStream共用一个Fork/Join线程池,因此会需要避免向其中提交耗时较长的任务。
其实也不建议在生产代码中使用parallelStream,会引发不可预料的后果。还是老老实实地使用线程池更加靠谱。
4. Optional的使用
Optional对于需要判空的场景比较实用,能够有效地减少不必要的判空分支。
我们通过一个场景来解释如何较好地使用Optional。
在通过OPENAPI对外提供服务的场景中:
我们每个对外提供服务的接口可以理解成一个业务场景,每个业务场景在系统内部会有一组错误码,每个内部错误码通过OPENAPI返回给外部的错误码又各不相同。
所以,这涉及到三个层次:
- 业务场景
- 系统内错误码
- 外部错误码
反应到代码中,可以作如下抽象:
/**
* 业务场景到异常类型的映射关系
*/
private Map<OpenApiServiceScenario, Map<BizResultCode, OpenApiErrorInfo>> scenarioToErrorMapping;
由于在每一层Map的get操作中都可能会得到null,因此判空操作在所难免。
判空所需要的if语句数量和层级的数量成正相关,这对后续分支覆盖率的保证不利。
此时,就可以通过Optional来消除这些仅仅是为了判空的if:
/**
* 全局兜底对外暴露异常
*/
private static OpenApiErrorInfo defaultSystemError;
/**
* 根据场景&业务错误码映射对应的返回错误码
*/
private OpenApiErrorConfig openApiErrorConfig;
/**
* 获取映射后的返回业务错误码
*/
public OpenApiErrorInfo getBizCode(OpenApiServiceScenario scene, BizResultCode errorCode)
// 根据调用方获取对应所有场景的异常信息
return Optional.ofNullable(openApiErrorConfig)
.map(OpenApiErrorConfig::getScenarioToErrorMapping)
.map((mappings) -> mappings.get(scene))
.map((sceneMapping) -> sceneMapping.get(errorCode)).orElse(defaultSystemError);
以上代码中出现了3次map调用:
- 第一次map调用的条件是openApiErrorConfig不为null
- 第二次map调用的条件是OpenApiErrorConfig::getScenarioToErrorMapping不为null
- 第三次map调用的条件是mappings.get(scene)不为null
以上任何一次map调用如果返回了null,那么就会返回orElse里面的defaultSystemError,即全局兜底的一个错误码。
以上是关于JDK 8 函数式编程最佳实践的主要内容,如果未能解决你的问题,请参考以下文章