这是一个新的系列,主要讲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");}

包含两个参数的方法

BinaryOperatorfunctionAdd=(x,y)->x+y;Longresult=functionAdd.apply(1L,2L);

包含两个参数且对参数显式声明

BinaryOperatorfunctionAdd=(Longx,Longy)->x+y;Longresult=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::staticMethodNameeg:Integer::valueOf

2.指向任意类型实例方法的方法引用:当直接调用对象的实例方法,则使用如下方式进行调用

表达式:(args)->args.instanceMethod();格式:ClassName::instanceMethod;eg:String::indexOfString::toString

3.指向现有对象的实例方法的方法引用:通过对象实例,方法引用实例方法

表达式:(args)->object.instanceMethod(args);改写为(args)->object::instanceMethod;eg:StringBuildersb=newStringBuilder();Consumerconsumer=(Stringstr)->stringBuilder.append(str);

就可以改写为

Consumerconsumer=(Stringstr)->stringBuilder::append;

2.从一个案例入手

我们先看一个例子,宏观感受一下Java8 Lambda编程带来的便利(后续讲解Stream同样使用该案例)

2.1 案例:直观体验Java8Stream操作

Sku实体类: 标识一个电商下单商品信息对象

publicclassSku{//商品编号privateIntegerskuId;//商品名称privateStringskuName;//单价privateDoubleskuPrice;//购买个数privateIntegertotalNum;//总价privateDoubletotalPrice;//商品类型privateEnumskuCategory;publicSku(){}publicSku(IntegerskuId,StringskuName,DoubleskuPrice,IntegertotalNum,DoubletotalPrice,EnumskuCategory){this.skuId=skuId;this.skuName=skuName;this.skuPrice=skuPrice;this.totalNum=totalNum;this.totalPrice=totalPrice;this.skuCategory=skuCategory;}...省略gettersetter...}

SkuCategoryEnum枚举类: 商品类型枚举

publicenumSkuCategoryEnum{CLOTHING(10,"服务类"),ELECTRONICS(20,"数码产品类"),SPORTS(30,"运动类"),BOOKS(40,"图书类");//商品类型编号privateIntegercode;//商品名称privateStringname;SkuCategoryEnum(Integercode,Stringname){this.code=code;this.name=name;}...省略getter...}

CartService类: 初始化一批数据,模拟购物车

publicclassCartService{//初始化购物车privatestaticListcartSkuList=newArrayList<>();static{cartSkuList.add(newSku(2,"无人机",1000.00,10,1000.00,SkuCategoryEnum.ELECTRONICS));cartSkuList.add(newSku(1,"VR一体机",2100.00,10,2100.00,SkuCategoryEnum.ELECTRONICS));cartSkuList.add(newSku(4,"牛仔裤",60.00,10,60.00,SkuCategoryEnum.CLOTHING));cartSkuList.add(newSku(13,"衬衫",120.00,10,120.00,SkuCategoryEnum.CLOTHING));cartSkuList.add(newSku(121,"Java编程思想",100.00,10,100.00,SkuCategoryEnum.BOOKS));cartSkuList.add(newSku(3,"程序化广告",80.00,10,80.00,SkuCategoryEnum.BOOKS));}publicstaticListgetCartSkuList(){returncartSkuList;}}

我们直接看这个案例

privatestaticListcartSkuList=CartService.getCartSkuList();@Testpublicvoidshow(){Listcollect=cartSkuList.stream()//方法引用.map(Sku::getSkuId).distinct().sorted().collect(Collectors.toList());collect.stream().forEach(skuId->{System.out.println(skuId.toString());});}

简单解释下这段代码的意图:

首先获取购物车中商品列表,将该列表转换为流;收集商品列表中的所有商品编号(skuId),对商品编号进行去重,并进行自然排序(升序排列),最后收集为一个商品编号集合,并对该集合进行遍历打印。

我并没有加注释,但是相信聪明的你也一定能读懂上面这段代码,这正是Stream编程的特点:方法名见名知意,流式编程方式符合人类思考逻辑

运行该用例,打印如下:

123413121

打印结果符合我们的预期意图。

想象一下,如果不使用lambda+Stream方式,而是使用java7及之前的传统集合操作,实现上述操作我们的代码量有多少?保守估计至少是上述代码段的1.5倍。

这个案例可能还不具备说服力,接下来的文章中,我将通过一个对比案例来比较lambda编程与传统方式对集合操作的效率提升。

那么,使用了lambda函数式编程之后,对我们的开发真有显著的提升么?

在接下来的章节中,我们通过一个实战案例对比原始集合操作与Stream集合操作具体有哪些不同,直观地展示Stream集合操作对编程效率的提升。

案例:对比原始集合操作与Stream集合操作需求场景:

针对上面的购物车,我们想要

全局查看购物车中都有哪些商品将购物车中的图书类商品进行过滤(删除图书类商品)在其余商品中挑选两件最贵的打印出上述两件商品的名称和总价原始集合操作:

@TestpublicvoidtraditionalWay(){//1.打印所有商品Listskus=CartService.getCartSkuList();for(Skusku:skus){System.out.println(JSON.toJSONString(sku,true));}//2.过滤图书类商品ListnotIncludeBooksList=newArrayList<>();for(Skusku:skus){if(!sku.getSkuCategory().equals(SkuCategoryEnum.BOOKS)){notIncludeBooksList.add(sku);}}//3.其余商品中挑选两件最贵的价格倒排序,取top2//3.1先排序notIncludeBooksList.sort(newComparator(){@Overridepublicintcompare(Skusku0,Skusku1){if(sku0.getTotalPrice()>sku1.getTotalPrice()){return-1;}if(sku0.getTotalPrice()top2SkuList=newArrayList<>();for(inti=0;i<2;i++){top2SkuList.add(notIncludeBooksList.get(i));}//4.打印出上述两件商品的名称和总价//4.1求两件商品总价doubletotalMoney=0.0;for(Skusku:top2SkuList){totalMoney+=sku.getTotalPrice();}//4.2获取两件商品名称ListresultSkuNameList=newArrayList<>();for(Skusku: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集合操作:

@TestpublicvoidstreamWay(){AtomicReferencemoney=newAtomicReference<>(Double.valueOf(0.0));ListresultSkuNameList=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高效编程之门》慕课网」

推荐内容