聊聊Java8中的函数式编程
# 写在文章开头
今天来聊聊Java8中的函数式编程,通过既定的语法糖简化并提升代码的语义,而本文将从几个比较常见的函数式接口开始阐述Java8中的函数式编程。

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili 。
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 详解lambda中的函数式编程
# 三大函数式接口
我们先来聊聊第一个函数式接口Predicate,它由FunctionalInterface标记为函数式接口,其内部唯一一个抽象方法为test,要求传入一个T类型的示例,返回boolean 类型:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
//......
}
2
3
4
5
6
7
根据这个接口的抽象方法,我们可以生成无数种用于断言的Predicate,假设我们希望判断一个字符串的长度是否大于12,那么我就可以根据抽象方法的要求生成一个匿名类:
Predicate<String> predicate = new Predicate<String>() {
@Override
public boolean test(String string) {
return string.length() > 12;
}
};
System.out.println(predicate.test("hello"));
2
3
4
5
6
7
8
为了实现抽象行为却要声明如此繁重的语句显得Java这门语言非常笨重,接下来我们就不妨一步步实现将函数式接口Predicate精简为lambda语法,首先我们根据Predicate的抽象接口得出,它要求传入一个T类型,以我们的代码为例就是String类型,返回一个Boolean类型的值,所以我们可以得出这个函数式接口的描述符为T->boolean,对应我们的例子就是String->boolean:

按照lambda的语法要求,对应表达式的格式为参数列表->{lambda代码段},所以我们简化后的表示是为:
Predicate<String> predicate = (String s)->{return s.length()>12;};
因为是Predicate是函数式接口,其抽象方法只有唯一一个test,所以我们的参数列表即使不指明类型,它也能够通过类型推断机制得出参数类型,所以我们可以直接将参数类型简写为s。
Predicate<String> predicate = (s)->{return s.length()>12;};
又因为lambda语句块中只有一段代码,按照语法糖要求我们可以直接将花括号和return以及封号全部去掉,由此我们得到一条最精简的lambda表达式:
Predicate<String> predicate = (s) -> s.length() > 12;
有了上述编写函数式接口lambda表达式的基础,我们再来了解一个Consumer函数式接口,对应代码如下所示:
@FunctionalInterface
public interface Consumer<T> {
//......
void accept(T t);
}
2
3
4
5
6
按照上文的步骤,我们也可以非常快速的得出这个接口的函数描述符为T->Void类型:

基于这个函数式接口,笔者给出这样一个场景,我们有一个方法会遍历一个整数的list集合的元素,但是具体对list做哪些处理是可变的(可以是输出原值,也可能是输出+1后的值),读者可以尝试基于Consumer完成这个需求。
这里笔者直接给出思路,按照需求说明,我们要求传入一个整数的list进行遍历,但是遍历后针对每一个元素进行个性化逻辑输出,由此得出这个需求要求的可变行为是Integer->Void,

由此我们可以得出这样一个方法:
public static <T> void forEach(List<Integer> list, Consumer<Integer> consumer) {
for (Integer t : list) {
//具体实现交由传入的消费者consumer
consumer.accept(t);
}
}
2
3
4
5
6
这样一来,按照forEach方法中Consumer的签名我们传入常规表达式(i)-> System.out.println(i)即可完成所有元素的打印和输出:
List<Integer> integerList=new ArrayList<>();
integerList.add(1);
integerList.add(2);
integerList.add(3);
integerList.add(4);
forEach(integerList,(i)-> System.out.println(i));
2
3
4
5
6
最后我们再给出第三个函数式接口Function,对应源码如下,不难看出这个接口的函数描述符为T->R:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
//......
}
2
3
4
5
假如我们要求传入一个字符串,返回它的长度或者字符串第1位unocode码值,这就是典型的String->Integer,那么我们就可以使用Function做到,还是老规矩先定义使用的方法,将返回长度或者unocode码值的逻辑抽象到Function接口中:
//将String ->Integer的行为抽象到function中
public static Integer StringCalculate(String s, Function<String,Integer> function){
return function.apply(s);
}
2
3
4
这样一来,我们的行为就可以通过lambda表达式作为行为传入:
StringCalculate("hello",(s)->s.length());
# 函数式接口更多的延申
基于上述三大函数式接口延申很多特殊类型的接口,读者可自行参阅,这里笔者就简单介绍一下原始类型特化的函数式接口,它是Java 8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作:


# lambda类型推断机制
我们一直提到一个类型推断的概念,它是编译器能够识别我们的lambda表达式并代入到函数式接口只执行的根本原因,我们还是以上文Predicate<String>接口对应的(s) -> s.length() > 12;介绍一下类型推断的过程。
可以看到Predicate<String>接口按照泛型指定的类型推断出函数描述符为String->boolean,对应的我们编写lambda代入这个函数描述后:
- 参数列表参数数一致。
- 参数类类型为
String成立。 - 返回值确实是
boolean。

由此类型推断成功,这个lambda成立。
# 不同的表达式可应用于不同的接口
由于lambda对参数列表和返回值类型的精简,使得我们的表达式能够符合函数式接口签名的要求,就能够完成编译和运行。
如下代码所示,根据上文的介绍我们都知道Comparator接口的函数签名为(T,T)->Integer,对应我们的Apple比较器则是(Apple,Apple)->Integer,同理BiFunction对应的抽象方法为R apply(T t, U u);,由此我们得出它的签名为(T,U)->R,很明显我们把T和U的类型都设置为Apple,而R类型设置为Integer,那么这两个函数式接口的函数描述符就是一致的,这时候我们同样的表达式就可以应用于两个函数式接口了:

基于我们上述的推到最终得出了两个都可应用的lambda表达式:
Comparator<Apple> comparator = (a1,a2) -> a1.getWeight() - a2.getWeight();
BiFunction<Apple,Apple,Integer> compare = (a1,a2) -> a1.getWeight() - a2.getWeight();
2
# lambda表达式对于外部变量的使用注意事项
我们键入下面这段代码时,会受到Variable used in lambda expression should be final or effectively final的报错,它要求我们必须使用final关键字来修饰。
这个问题也很好解释,本质上局部变量num分配在栈上,是可以随时改变且随着当前函数的执行就会被回收。而lambda只允许捕获变量一次,这就会导致以下两种情况:
- 我们的
Runnable是异步执行的,如果允许使用这个局部变量的话,就可能出现num被回收后Runnable才开始运行,由此引发一系列灾难性问题。 num仅仅被捕获一次,后续的修改操作对lambda不可见。
int num=1;
Runnable r=()-> System.out.println(num);
r.run();
num=2;
2
3
4
通过final关键字修饰后,保证局部变量的不可变性,从而保证lambda引用时可以拷贝一份进行使用:
final int num=1;
Runnable r=()-> System.out.println(num);
r.run();
2
3
而实例变量是分配在堆上,不会被回收,所以lambda表达式可以直接引用实例变量:
Apple apple=new Apple(1);
Runnable r=()-> System.out.println(apple.getWeight());
r.run();
2
3
# 函数式接口在开源项目中的运用
由于函数式接口的理念,保证了行为抽象和实现上的便利,大量的开源项目都运用到了函数式编程风格,最经典的就是mybatis-plus中对于批量保存操作的封装,这一点我们直接打开ServiceImpl的saveBatch方法可以看到一个executeBatch方法,可以看到它将待保存入库的list和消费list集合的函数式接口consumer作为参数传入SqlHelper的executeBatch方法:
protected <E> boolean executeBatch(Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
return SqlHelper.executeBatch(this.entityClass, this.log, list, batchSize, consumer);
}
2
3
步入SqlHelper的executeBatch方法可以看到,其内部本质就是调用函数式接口的consumer的accept方法消费list中的每一个元素,而消费的行为accept则交由外部调用者实现:
public static <E> boolean executeBatch(Class<?> entityClass, Log log, Collection<E> list, int batchSize, BiConsumer<SqlSession, E> consumer) {
//......
return !CollectionUtils.isEmpty(list) && executeBatch(entityClass, log, sqlSession -> {
int size = list.size();
int i = 1;
//遍历list
for (E element : list) {
//基于外部传入的consumer的accept方法对遍历元素进行消费
consumer.accept(sqlSession, element);
if ((i % batchSize == 0) || i == size) {
sqlSession.flushStatements();
}
i++;
}
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
根据executeBatch方法的入参BiConsumer<SqlSession, E> consumer,可知这个函数式接口的签名为(SqlSession,E)->void,所以回到saveBatch方法即可看到它缩写的表达式是(sqlSession, entity) -> sqlSession.insert(sqlStatement, entity),即通过sqlSession和entity作为参数,然后调用sqlSession的insert将entity保存,无返回值:
@Transactional(rollbackFor = Exception.class)
@Override
public boolean saveBatch(Collection<T> entityList, int batchSize) {
String sqlStatement = getSqlStatement(SqlMethod.INSERT_ONE);
//基于签名(SqlSession,E)->void 得出(sqlSession, entity) -> sqlSession.insert(sqlStatement, entity)
return executeBatch(entityList, batchSize, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
}
2
3
4
5
6
7
# 小结
自此我们从几个实践案例和开源项目的设计中了解到函数式接口的编程理念,这里我们简单小结一下函数式编程的使用方式:
- 梳理需求,得出需求中可变和不可变的点。
- 得出参数列表中固定参数和可变参数。
- 将可变参数封装成函数式接口。
- 实现需求逻辑。
- 基于函数式接口得出函数签名,基于此签名对不同业务场景使用不同lambda作为参数传入。
我是 sharkchili ,CSDN Java 领域博客专家,开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili 。 因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

# 参考
《java8 实战》