Java函数式编程学习笔记(一)
前言
最近在项目中需要使用一些基于MyBatis-Plus封装的CRUD接口,阅读一些文档和代码后发现在Java代码中偶尔会见到C++代码里的类作用域符号::
和->
运算符,查阅资料才了解到这两个符号在Java领域里属于函数式编程的语法内容,于是写下这篇博客以记录这段时间的学习过程。
Java函数式编程
什么是函数式编程?
函数式编程是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种函数我们称为没有副作用的函数。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出,因此,这种函数是有副作用的。
函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数。函数式编程是把函数作为基本运算单元,函数可以作为变量,可以接收函数,还可以返回函数。
历史上研究函数式编程的理论是Lambda演算,所以我们经常把支持函数式编程的编码风格称为Lambda表达式。
Lambda表达式
Lambda表达式是表示可传递匿名函数的一种简洁方式,Lambda表达式没有名称,但是有参数列表、函数主体、返回类型,还可能有一个可以抛出的异常列表。它是Java 8新增的特性,有了它,我们再也不用像以前那样写一堆笨重的匿名类代码了。
在Java中,我们经常遇到单方法接口,即一个接口只定义了一个方法,例如:
Comparator
Runnable
Callable
对于单方法接口,我们称之为FunctionalInterface
,用注解@FunctionalInterface
标记。以Comparator
为例,我们想要调用Arrays.sort()
时,可以传入一个Comparator
实例,以匿名类方式编写如下:
1 | Arrays.sort(array, new Comparator<String>() { |
上述写法非常繁琐。从Java 8开始,我们可以用Lambda表达式替换单方法接口。改写上述代码如下:
1 | Arrays.sort(array, (s1, s2) -> { |
观察Lambda表达式的写法,它只需要写出方法定义:
1 | (s1, s2) -> { |
其中,参数是(s1, s2)
,参数类型可以省略,因为编译器可以自动推断出String
类型。-> { ... }
表示方法体,所有代码写在内部即可。Lambda表达式没有class
定义,因此写法非常简洁。
如果只有一行return ...
的代码,可以用更简单的写法,即省略方法体的括号:
Arrays.sort(array, (s1, s2) -> s1.compareTo(s2)); |
方法引用
方法引用是Java8中引入的新特性,它提供了一种引用方法而不执行方法的方式,可以让我们重复使用现用方法的定义,作为某些Lambda表达式的另一种更简洁的写法。
当你需要方法引用时,将目标引用放在分隔符::
前,方法的名称放在分隔符::
后。方法名称后不需要加括号,因为我们并没有实际调用它。方法引用提高了代码的可读性,也使逻辑更加清晰。
可以构建方法引用的场景有四种:
静态方法
指向静态方法的引用,语法:类名::静态方法名
,类名放在分隔符::
前,静态方法名放在分隔符::
后。例如:
(String str) -> Integer.parseInt(str) |
使用方法引用以后,可以简写为:
Integer::parseInt |
内部对象的实例方法
指向Lambda表达式内部对象的实例方法的引用,语法:类名::实例方法名
,类名放在分隔符::
前,实例方法名放在分隔符::
后。例如:
(Equipment equipment) -> equipment.getBrand() |
使用方法引用以后,可以简写为:
Equipment::getBrand |
外部对象的实例方法
指向Lambda表达式外部对象的实例方法的引用,语法:实例名::实例方法名
,类名放在分隔符::
前,实例方法名放在分隔符::
后。例如:
1 | String type = "STR"; |
其中,type
是一个Lambda表达式外部的局部变量,使用方法引用以后,可以简写为:
1 | String type = "STR"; |
构造方法
指向构造方法的引用,语法:类名::new
,类名放在分隔符::
前,new放在分隔符::
后。例如:
(String brand, String type) -> new Equipment(brand, type) |
使用方法引用以后,可以简写为:
Equipment::new |
应用
背景
在一个数据库表中,存在一对多的E-R关系,对父表新增一条数据时,子表的若干条数据需要关联父表的这行数据;进一步地,需要在对父表删除一行数据时,与其关联的若干条要被同步删除,以保证数据之间的约束。
思路
首先分析父表与子表的关联关系,找出子表是与父表的哪一个字段关联的,这样即可根据父表的字段找到子表的数据,随后即可删除子表的数据。在编写业务逻辑时需注意:先删除子表数据,后删除父表数据。
应用场景
使用其他业务方法得到一个由子表数据组成的实体数组时,需要根据每个实体的id
进行删除。传统的写法是使用for
循环遍历这个数组,在循环体中写一行sql语句删除逐行数据,这种写法需要创建多个临时变量储存临时数据,较为繁琐,也显得代码块较为臃肿。利用Java函数式编程的新特性即可简化代码。
解决方案
创建Stream
储存该实体数组进行流式处理,代替for
循环,使用map
将数组中的每个实体转换为实体对应的id
。map
方法又可接收Function
接口对象的方法引用,无需创建实例即可引用方法,进一步减少了代码量。
实例
假设已得到一个实体数组:
List<EmissionCalcParamDTO> emissionCalcParams |
现调用一个入参为数组的deleteBatchIds()
方法,它是MyBatis-Plus提供的接口,用于删除多行数据。调用前需先判断数组是否为空,写法如下:
1 | if (CollectionUtils.isNotEmpty(emissionCalcParams)) { |
上面的代码在把实体转换为id
后又将其组合成数组,作为deleteBatchIds()
的入参,可以说用一行代码解决了传统写法十几行的工作量。
注意,上述编码过程中还使用到了
Stream
流和map
方法,并使用MyBatis-Plus
简化数据库操作,上述代码基于Spring Boot
框架进行编写。对于上述使用到的技术,我将在后面的文章中详细介绍。
非常感谢你的阅读,辛苦了!
参考文章: (感谢以下资料提供的帮助)