Java之函数式编程

Java从1.8以后引入了函数式编程,这是很大的一个改进。函数式编程的优点在提高编码的效率,增强代码的可读性。

先了解Java新引入的函数式编程,首先了解在没有函数式编程之前,函数编程的演变史。

实战案例:函数编程演变史

根据实际场景演示,为了应对不断变化的业务需求,代码的多个改进版本。

购物车案例

准备前置需求,购物车相关的实体类、枚举类、服务类等

下单商品实体类

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
53
54
55
56
package com.function.shopping.cart;

/**
* 下单商品信息对象
*
* @author jinglv
* @date 2021/6/21 1:19 下午
*/
public class Sku {
/**
* 编号
*/
private Integer skuId;
/**
* 商品名称
*/
private String skuName;
/**
* 单价
*/
private Double skuPrice;
/**
* 购买个数
*/
private Integer totalNum;
/**
* 总价
*/
private Double totalPrice;
/**
* 商品类型
*/
private Enum skuCategory;

/**
* 构造函数
*
* @param skuId
* @param skuName
* @param skuPrice
* @param totalNum
* @param totalPrice
* @param skuCategory
*/
public Sku(Integer skuId, String skuName, Double skuPrice, Integer totalNum, Double totalPrice, Enum skuCategory) {
this.skuId = skuId;
this.skuName = skuName;
this.skuPrice = skuPrice;
this.totalNum = totalNum;
this.totalPrice = totalPrice;
this.skuCategory = skuCategory;
}

getter/setter...
}

商品类型枚举

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
53
54
55
package com.function.shopping.enums;

/**
* 商品类型枚举
*
* @author jinglv
* @date 2021/6/21 1:25 下午
*/
public enum SkuCategoryEnum {
/**
* 商品类型-服装类
*/
CLOTHING(10, "服装类"),
/**
* 商品类型-数码类
*/
ELECTRONICS(30, "数码类"),
/**
* 商品类型-运动类
*/
SPORTS(30, "运动类"),
/**
* 商品类型-图书类
*/
BOOKS(40, "图书类");

/**
* 商品类型的编号
*/
private final Integer code;

/**
* 商品类型的名称
*/
private final String name;

/**
* 构造方法
*
* @param code
* @param name
*/
SkuCategoryEnum(Integer code, String name) {
this.code = code;
this.name = name;
}

public Integer getCode() {
return code;
}

public String getName() {
return name;
}
}

购物车服务类

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
package com.function.shopping.service;

import com.function.shopping.cart.Sku;
import com.function.shopping.enums.SkuCategoryEnum;

import java.util.ArrayList;
import java.util.List;

/**
* 购物车服务类
*
* @author jinglv
* @date 2021/6/21 1:23 下午
*/
public class CartService {
/**
* 初始化商品数据-加入到购物车中的商品信息
*/
private static List<Sku> cartSkuList = new ArrayList<Sku>() {
{
add(new Sku(654032, "无人机", 4999.00, 1, 4999.00, SkuCategoryEnum.ELECTRONICS));
add(new Sku(654033, "VR一体机", 2299.00, 1, 2299.00, SkuCategoryEnum.ELECTRONICS));
add(new Sku(654221, "跑步机", 3999.00, 1, 3999.00, SkuCategoryEnum.SPORTS));
add(new Sku(654121, "Java编程思想", 79.80, 1, 79.80, SkuCategoryEnum.BOOKS));
add(new Sku(654122, "Java核心技术", 149.80, 1, 149.80, SkuCategoryEnum.BOOKS));
add(new Sku(654021, "睡衣套装", 299.00, 1, 299.00, SkuCategoryEnum.CLOTHING));
add(new Sku(654022, "牛仔裤", 1999.00, 1, 1999.00, SkuCategoryEnum.CLOTHING));
}
};

/**
* 获取商品列表
*
* @return 返回商品列表
*/
public static List<Sku> getCartSKuList() {
return cartSkuList;
}
}

需求实现

version1.0.0

需求:找出购物车中所有电子产品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* version 1.0.0
* 找出购物车中所有电子产品
*
* @param cartSkuList 商品列表
* @return 返回商品列表
*/
public static List<Sku> filterElectronicsSkus(List<Sku> cartSkuList) {
List<Sku> result = new ArrayList<>();
for (Sku sku : cartSkuList) {
// 如果商品类型等于电子类,加入到结果集合
if (SkuCategoryEnum.ELECTRONICS.equals(sku.getSkuCategory())) {
result.add(sku);
}
}
return result;
}

测试代码

1
2
3
4
5
6
7
8
@Test
void filterElectronicsSkus() {
// 获取购物车中的商品列表
List<Sku> skus = CartService.getCartSKuList();
// 查找购物车中数码商品
List<Sku> result = CartService.filterElectronicsSkus(skus);
System.out.println(JSONUtil.parse(result));
}

version2.0.0

需求:根据传入商品类型参数,找出购物车中同种商品类型的商品列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* version2.0.0
* 根据传入商品类型参数,找出购物车中同种商品类型的商品列表
*
* @param cartSkuList 商品列表
* @param skuCategoryEnum 商品类型
* @return 返回已处理的商品列表
*/
public static List<Sku> filterSkusByCategory(List<Sku> cartSkuList, SkuCategoryEnum skuCategoryEnum) {
List<Sku> result = new ArrayList<>();
for (Sku sku : cartSkuList) {
// 如果商品类型等于传入的商品类型,加入到结果集合
if (skuCategoryEnum.equals(sku.getSkuCategory())) {
result.add(sku);
}
}
return result;
}

测试代码

1
2
3
4
5
6
7
8
@Test
void filterSkusByCategory() {
// 获取购物车中的商品列表
List<Sku> skus = CartService.getCartSKuList();
// 查找购物车中图书类商品集合
List<Sku> result = CartService.filterSkusByCategory(skus, SkuCategoryEnum.BOOKS);
System.out.println(JSONUtil.parse(result));
}

version3.0.0

支持通过商品类型或总价过滤商品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* version 3.0.0
* 支持通过商品类型或总价过滤商品
*
* @param cartSkuList 商品列表
* @param skuCategoryEnum 商品枚举
* @param totalPrice 总价
* @param categoryOrPrice true:根据商品类型 false:根据商品总价
* @return 返回过滤完成商品列表
*/
public static List<Sku> filterSkusByPrice(List<Sku> cartSkuList, SkuCategoryEnum skuCategoryEnum, Double totalPrice, Boolean categoryOrPrice) {
List<Sku> result = new ArrayList<>();
for (Sku sku : cartSkuList) {
// 如果根据商品类型判断,sku类型与输入类型比较
// 如果根据商品总价判断,sku总价与输入总价比较
if ((categoryOrPrice && skuCategoryEnum.equals(sku.getSkuCategory())) || (!categoryOrPrice && sku.getTotalPrice() > totalPrice)) {
result.add(sku);
}
}
return result;
}

测试代码

1
2
3
4
5
6
7
8
@Test
void filterSkusByPrice() {
// 获取购物车中的商品列表
List<Sku> skus = CartService.getCartSKuList();
// 根据商品总价过滤超过2000元的商品列表
List<Sku> result = CartService.filterSkusByPrice(skus, null, 2000.00, false);
System.out.println(JSONUtil.parse(result));
}

小结

已经完成了三个需求的比较,发现根据条件过滤,只要条件(单条件或多条件)不同就会写相应的方法,因此会在代码中硬编码大量的判断条件,带来了代码不易阅读及难以维护,下面的版本展示优化硬编码带来的问题

version4.0.0

根据不同的Sku判断标准,对Sku列表进行过滤

传入一个判断标准,判断标准会动态的判断条件是否满足条件,从而是否达到条件进行返回,无形的将判断条件进行参数化,通过参数传入一个接口,接口的实现类定义了条件判断的具体实现,行为参数化传入到方法中,这其实是设计模式中的策略模式将数据的循环与处理分开了

1.定义Sku的选择谓词接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.function.shopping.service;

import com.function.shopping.cart.Sku;

/**
* Sku选择谓词接口
*
* @author jinglv
* @date 2021/7/5 8:49 下午
*/
public interface SkuPredicate {
/**
* 选择判断标准
*
* @param sku 商品
* @return boolean
*/
boolean test(Sku sku);
}
2.实现接口条件判断的具体实现

条件一:对Sku的总价是否超出2000作为的判断标准

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.function.shopping.predicate;

import com.function.shopping.cart.Sku;
import com.function.shopping.service.SkuPredicate;

/**
* 对Sku的总价是否超出2000作为判断标准
*
* @author jinglv
* @date 2021/7/5 8:58 下午
*/
public class SkuTotalPricePredicate implements SkuPredicate {
@Override
public boolean test(Sku sku) {
return sku.getTotalPrice() > 2000;
}
}

条件二:对Sku的商品类型为图书类的判断标准

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.function.shopping.predicate;

import com.function.shopping.cart.Sku;
import com.function.shopping.enums.SkuCategoryEnum;
import com.function.shopping.service.SkuPredicate;

/**
* 对Sku商品类型为图书类的判断标准
*
* @author jinglv
* @date 2021/7/5 8:56 下午
*/
public class SkuBooksCategoryPredicate implements SkuPredicate {
@Override
public boolean test(Sku sku) {
return SkuCategoryEnum.BOOKS.equals(sku.getSkuCategory());
}
}

3.具体使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* version 4.0.0
* 根据不同的Sku判断标准,对Sku列表进行过滤
*
* @param cartSkuList 商品列表
* @param skuPredicate 不同的SKu判断标准
* @return sku List
*/
public static List<Sku> filterSkus(List<Sku> cartSkuList, SkuPredicate skuPredicate) {
List<Sku> result = new ArrayList<>();
for (Sku sku : cartSkuList) {
// 根据不同的sku判断标准策略,对Sku进行判断
if (skuPredicate.test(sku)) {
result.add(sku);
}
}
return result;
}

测试代码

1
2
3
4
5
6
7
8
@Test
void filterSkus() {
// 获取购物车中的商品列表
List<Sku> skus = CartService.getCartSKuList();
// 过滤商品总价大于2000的商品
List<Sku> result = CartService.filterSkus(skus, new SkuTotalPricePredicate());
System.out.println(JSONUtil.parse(result));
}

version5.0.0

使用策略模式将数据的循环与处理区分开,但是有带来一个问题,编写的判断条件有且只是用一次,若条件过多,我们会编写很多的判断条件类实现Predicate接口,而每次只是用一次,因此可以引用内部匿名类进行优化

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
void filterSkusAnonymous() {
// 获取购物车中的商品列表
List<Sku> skus = CartService.getCartSKuList();
// 过滤商品总价大于1000的商品
List<Sku> result = CartService.filterSkus(
skus, new SkuPredicate() {
@Override
public boolean test(Sku sku) {
return sku.getSkuPrice() > 1000;
}
}
);
System.out.println(JSONUtil.parse(result));
}

version6.0.0

在第五版的迭代后,使用了匿名类进行代码优化,好处是不用对接口进行实现,不用产生多余的类,只是用一次也没必要些那么多的类,但是在代码中的匿名类在写起来还是有些多,本次版本优化使用lambda进行优化

测试代码

1
2
3
4
5
6
7
8
9
@Test
void filterSkusLambda() {
// 获取购物车中的商品列表
List<Sku> skus = CartService.getCartSKuList();
// 过滤商品总价大于1000的商品
List<Sku> result = CartService.filterSkus(
skus, (Sku sku) -> sku.getSkuPrice() > 1000);
System.out.println(JSONUtil.parse(result));
}

这段代码与version6.0.0执行效果完全一样,但代码编写上出现了很大的不同,使用Lambda表达式替代匿名类,将行为参数化传递到代码中,由代码动态调用。

函数编程演化历程

  1. 将业务逻辑直接写死在代码里
  2. 将单一维度的条件作为参数传入方法中。方法内根据参数进行业务逻辑实现。
  3. 将多个纬度的条件作为参数传入方法中。业务实现需要根据不同的参数处理不同逻辑。
  4. 将业务逻辑封装为一个实体类,方法接收实体类为参数,方法内部调用实体类的处理逻辑。
  5. 调用方法时不在创建实体类,而是使用匿名函数的形式替代。
  6. 使用Lambda表达式替代匿名函数的形式,作为方法的参数。真正实现判断逻辑参数化传递。