Java8函数式编程真有这么神奇?- 案例展示lambda表达式对编码效率提升到底有多大
这是一个新的系列,主要讲Java8的lambda编程以及Stream流式编程相关的用法和案例。
这个系列脱胎于一个内部的分享,由于篇幅较长,内容较多,因此拆分成多篇文章进行发布,方便自己后续参考,也希望能够帮到读者朋友。
本文是Java8函数式编程系列的第一篇,我们一起学习一下Java8函数式编程的基本概念及操作。
1.概述Lambda表达式
❝Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。
Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。
❞
使用 Lambda 表达式可以使代码变的更加简洁紧凑。
1.1 lambda表达式语法
(parameters) -> expression
或
(parameters) -> {statement};
我们可以将lambda表达式理解为一种代替原先匿名函数的新的编程方式
通过使用lambda表达式替换匿名函数的形式,将lambda表达式作为方法参数,实现判断逻辑参数化传递的目的。
1.2 lambda表达式形式
无参数
() -> System.out.println("code");
只有一个参数
name -> System.out.println("hello:" + name + "!");
没有参数,且逻辑复杂,需要通过大括号将多个语句括起来
() -> {
System.out.println("hello");
System.out.println("lambda");
}
包含两个参数的方法
BinaryOperator functionAdd = (x, y) -> x + y;
Long result = functionAdd.apply(1L, 2L);
包含两个参数且对参数显式声明
BinaryOperator functionAdd = (Long x, Long y) -> x + y;
Long result = functionAdd.apply(1L, 2L);
1.3 函数式接口
定义:
❝一个接口有且只有一个抽象方法;
❞
函数式接口的实例可以通过 lambda 表达式、方法引用或者构造方法引用来创建;
「注意」:
如果一个接口只有一个抽象方法,那么该接口就是一个函数式接口
如果我们在某个接口上声明了 「@FunctionalInterface」 注解,那么编译器就会按照函数式接口的定义来要求该接口
@FunctionInterface是Java8函数式接口注解,属于声明式注解,帮助编译器校验被标注的接口是否符合函数式接口定义
1.4 方法引用
我们通过Lambda表达式来实现匿名方法。
有些情况下,使用Lambda表达式仅仅是调用一些已经存在的方法;除了调用动作外,没有其他任何多余的动作,在这种情况下我们倾向于通过方法名来调用它,而Lambda表达式可以帮助我们实现这一要求。它使得Lambda在调用那些已经拥有方法名的方法的代码更简洁、更容易理解。
方法引用可以理解为Lambda表达式的另外一种表现形式。
❝方法引用是调用特定方法的lambda表达式的一种快捷写法,可以让你重复使用现有的方法定义,并像lambda表达式一样传递他们。
❞
注意:
使用方法引用时,只写方法名,不写括号
1.4.1 方法引用格式:
格式: 目标引用 双冒号分隔符 方法名
eg: String :: valueOf
1.4.2 方法引用分类:
1.指向静态方法的方法引用:当Lambda表达式的结构体是一个对象,并且调用其静态方法时,使用如下方式
表达式:
(args) -> ClassName::staticMethod(args);
格式: ClassName :: staticMethodName
eg: Integer :: valueOf
2.指向任意类型实例方法的方法引用:当直接调用对象的实例方法,则使用如下方式进行调用
表达式:
(args) -> args.instanceMethod();
格式: ClassName::instanceMethod;
eg: String::indexOf
String::toString
3.指向现有对象的实例方法的方法引用:通过对象实例,方法引用实例方法
表达式:
(args) -> object.instanceMethod(args);
改写为
(args) -> object::instanceMethod;
eg:
StringBuilder sb = new StringBuilder();
Consumer consumer = (String str) -> stringBuilder.append(str);
就可以改写为
Consumer consumer = (String str) -> stringBuilder::append;
2.从一个案例入手
我们先看一个例子,宏观感受一下Java8 Lambda编程带来的便利(后续讲解Stream同样使用该案例)
2.1 案例:直观体验Java8Stream操作
Sku实体类: 标识一个电商下单商品信息对象
public class Sku {
// 商品编号
private Integer skuId;
// 商品名称
private String skuName;
// 单价
private Double skuPrice;
// 购买个数
private Integer totalNum;
// 总价
private Double totalPrice;
// 商品类型
private Enum skuCategory;
public Sku() {
}
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...
}
SkuCategoryEnum枚举类: 商品类型枚举
public enum SkuCategoryEnum {
CLOTHING(10, "服务类"),
ELECTRONICS(20, "数码产品类"),
SPORTS(30, "运动类"),
BOOKS(40, "图书类")
;
// 商品类型编号
private Integer code;
// 商品名称
private String name;
SkuCategoryEnum(Integer code, String name) {
this.code = code;
this.name = name;
}
...省略getter...
}
CartService类: 初始化一批数据,模拟购物车
public class CartService {
// 初始化购物车
private static List cartSkuList = new ArrayList<>();
static {
cartSkuList.add(new Sku(2, "无人机", 1000.00, 10, 1000.00, SkuCategoryEnum.ELECTRONICS));
cartSkuList.add(new Sku(1, "VR一体机", 2100.00, 10, 2100.00, SkuCategoryEnum.ELECTRONICS));
cartSkuList.add(new Sku(4, "牛仔裤", 60.00, 10, 60.00, SkuCategoryEnum.CLOTHING));
cartSkuList.add(new Sku(13, "衬衫", 120.00, 10, 120.00, SkuCategoryEnum.CLOTHING));
cartSkuList.add(new Sku(121, "Java编程思想", 100.00, 10, 100.00, SkuCategoryEnum.BOOKS));
cartSkuList.add(new Sku(3, "程序化广告", 80.00, 10, 80.00, SkuCategoryEnum.BOOKS));
}
public static List getCartSkuList() {
return cartSkuList;
}
}
我们直接看这个案例
private static List cartSkuList = CartService.getCartSkuList();
@Test
public void show() {
List collect = cartSkuList.stream()
// 方法引用
.map(Sku::getSkuId)
.distinct()
.sorted()
.collect(Collectors.toList());
collect.stream().forEach(skuId -> {
System.out.println(skuId.toString());
});
}
简单解释下这段代码的意图:
首先获取购物车中商品列表,将该列表转换为流;收集商品列表中的所有商品编号(skuId),对商品编号进行去重,并进行自然排序(升序排列),最后收集为一个商品编号集合,并对该集合进行遍历打印。
我并没有加注释,但是相信聪明的你也一定能读懂上面这段代码,这正是Stream编程的特点:方法名见名知意,流式编程方式符合人类思考逻辑
运行该用例,打印如下:
1
2
3
4
13
121
打印结果符合我们的预期意图。
想象一下,如果不使用lambda+Stream方式,而是使用java7及之前的传统集合操作,实现上述操作我们的代码量有多少?保守估计至少是上述代码段的1.5倍。
这个案例可能还不具备说服力,接下来的文章中,我将通过一个对比案例来比较lambda编程与传统方式对集合操作的效率提升。
❝那么,使用了lambda函数式编程之后,对我们的开发真有显著的提升么?
❞
在接下来的章节中,我们通过一个实战案例对比原始集合操作与Stream集合操作具体有哪些不同,直观地展示Stream集合操作对编程效率的提升。
案例:对比原始集合操作与Stream集合操作
需求场景:
针对上面的购物车,我们想要
全局查看购物车中都有哪些商品 将购物车中的图书类商品进行过滤(删除图书类商品) 在其余商品中挑选两件最贵的 打印出上述两件商品的名称和总价
原始集合操作:
@Test
public void traditionalWay() {
// 1. 打印所有商品
List skus = CartService.getCartSkuList();
for (Sku sku : skus) {
System.out.println(JSON.toJSONString(sku, true));
}
// 2. 过滤图书类商品
List notIncludeBooksList = new ArrayList<>();
for (Sku sku : skus) {
if (!sku.getSkuCategory().equals(SkuCategoryEnum.BOOKS)) {
notIncludeBooksList.add(sku);
}
}
// 3. 其余商品中挑选两件最贵的 价格倒排序,取top2
// 3.1 先排序
notIncludeBooksList.sort(new Comparator() {
@Override
public int compare(Sku sku0, Sku sku1) {
if (sku0.getTotalPrice() > sku1.getTotalPrice()) {
return -1;
}
if (sku0.getTotalPrice() < sku1.getTotalPrice()) {
return 1;
}
return 0;
}
});
// 3.2 取top2
List top2SkuList = new ArrayList<>();
for (int i = 0; i < 2; i++) {
top2SkuList.add(notIncludeBooksList.get(i));
}
// 4. 打印出上述两件商品的名称和总价
// 4.1 求两件商品总价
double totalMoney = 0.0;
for (Sku sku : top2SkuList) {
totalMoney += sku.getTotalPrice();
}
// 4.2 获取两件商品名称
List resultSkuNameList = new ArrayList<>();
for (Sku sku : top2SkuList) {
resultSkuNameList.add(sku.getSkuName());
}
// 打印输出结果
System.out.println("结果商品名称: " + JSON.toJSONString(resultSkuNameList, true));
System.out.println("商品总价:" + totalMoney);
}
运行结果:
{"skuCategory":"ELECTRONICS","skuId":2,"skuName":"无人机","skuPrice":1000.0,"totalNum":10,"totalPrice":1000.0}
{"skuCategory":"ELECTRONICS","skuId":1,"skuName":"VR一体机","skuPrice":2100.0,"totalNum":10,"totalPrice":2100.0}
{"skuCategory":"CLOTHING","skuId":4,"skuName":"牛仔裤","skuPrice":60.0,"totalNum":10,"totalPrice":60.0}
{"skuCategory":"CLOTHING","skuId":13,"skuName":"衬衫","skuPrice":120.0,"totalNum":10,"totalPrice":120.0}
{"skuCategory":"BOOKS","skuId":121,"skuName":"Java编程思想","skuPrice":100.0,"totalNum":10,"totalPrice":100.0}
{"skuCategory":"BOOKS","skuId":3,"skuName":"程序化广告","skuPrice":80.0,"totalNum":10,"totalPrice":80.0}
结果商品名称: [
"VR一体机",
"无人机"
]
商品总价:3100.0
我们可以看到传统的集合操作还是写了比较多的代码,而且在编码过程中为了满足各种要求,我们通过声明新的容器来接受过程中的操作结果,这带来了内存使用量的增加。
接下来看一下Stream方式下如何编码实现我们的需求:
Stream集合操作:
@Test
public void streamWay() {
AtomicReference money = new AtomicReference<>(Double.valueOf(0.0));
List resultSkuNameList = CartService.getCartSkuList()
// 获取集合流
.stream()
/**1. 打印商品信息*/
.peek(sku -> System.out.println(JSON.toJSONString(sku)))
/**2. 过滤掉所有的图书类商品*/
.filter(sku -> !SkuCategoryEnum.BOOKS.equals(sku.getSkuCategory()))
/**3. 价格进行排序,默认是从小到大,调用reversed进行翻转排序即从大到小*/
.sorted(Comparator.comparing(Sku::getTotalPrice).reversed())
/**4. 取top2*/
.limit(2)
/**累加金额*/
.peek(sku -> money.set(money.get() + sku.getTotalPrice()))
/**获取商品名称*/
.map(sku -> sku.getSkuName())
.collect(Collectors.toList());
System.out.println("商品总价:" + money.get());
System.out.println("商品名列表:" + JSON.toJSONString(resultSkuNameList));
}
运行结果:
{"skuCategory":"ELECTRONICS","skuId":2,"skuName":"无人机","skuPrice":1000.0,"totalNum":10,"totalPrice":1000.0}
{"skuCategory":"ELECTRONICS","skuId":1,"skuName":"VR一体机","skuPrice":2100.0,"totalNum":10,"totalPrice":2100.0}
{"skuCategory":"CLOTHING","skuId":4,"skuName":"牛仔裤","skuPrice":60.0,"totalNum":10,"totalPrice":60.0}
{"skuCategory":"CLOTHING","skuId":13,"skuName":"衬衫","skuPrice":120.0,"totalNum":10,"totalPrice":120.0}
{"skuCategory":"BOOKS","skuId":121,"skuName":"Java编程思想","skuPrice":100.0,"totalNum":10,"totalPrice":100.0}
{"skuCategory":"BOOKS","skuId":3,"skuName":"程序化广告","skuPrice":80.0,"totalNum":10,"totalPrice":80.0}
商品总价:3100.0
商品名列表:["VR一体机","无人机"]
我们可以看到,通过Stream集合操作,运行结果与传统集合操作完全一致。但是编码量却能够显著减少。
「辩证的分析一下」,如果对Stream操作没有一个较为明确的了解,阅读这段代码确实有些难度,但是只要有一点了解,Stream集合操作代码带来的无论是编码量显著降低还是可读性提升,亦或是内存空间的节约都是可观的。
阶段小结
可见,学习并运用Lambda及Stream编程,对于提升我们的编码效率以及提升代码可读性都有着明显的收益。
参考资料
「《告别996,开启Java高效编程之门》慕课网」