还看不懂同事的代码Java8中超强的S

Java8新特性系列文章索引。

前言

我们都知道Lambda和Stream是Java8的两大亮点功能,在前面的文章里已经介绍过Lambda相关知识,这次介绍下Java8的Stream流操作。它完全不同于java.io包的Input/OutputStream,也不是大数据实时处理的Stream流。这个Stream流操作是Java8对集合操作功能的增强,专注于对集合的各种高效、便利、优雅的聚合操作。借助于Lambda表达式,显著的提高编程效率和可读性。且Stream提供了并行计算模式,可以简洁的编写出并行代码,能充分发挥如今计算机的多核处理优势。

在使用Stream流操作之前你应该先了解Lambda相关知识,如果还不了解,可以参考之前文章:还看不懂同事的代码?Lambda表达式、函数接口了解一下。

1.Stream流介绍

Stream不同于其他集合框架,它也不是某种数据结构,也不会保存数据,但是它负责相关计算,使用起来更像一个高级的迭代器。在之前的迭代器中,我们只能先遍历然后在执行业务操作,而现在只需要指定执行什么操作,Stream就会隐式的遍历然后做出想要的操作。另外Stream和迭代器一样的只能单向处理,如同奔腾长江之水一去而不复返。

由于Stream流提供了惰性计算和并行处理的能力,在使用并行计算方式时数据会被自动分解成多段然后并行处理,最后将结果汇总。所以Stream操作可以让程序运行变得更加高效。

2.Stream流概念

Stream流的使用总是按照一定的步骤进行,可以抽象出下面的使用流程。

数据源(source)-数据处理/转换(intermedia)-结果处理(terminal)

2.1.数据源

数据源(source)也就是数据的来源,可以通过多种方式获得Stream数据源,下面列举几种常见的获取方式。

Collection.stream();从集合获取流。

Collection.parallelStream();从集合获取并行流。

Arrays.stream(Tarray)orStream.of();从数组获取流。

BufferedReader.lines();从输入流中获取流。

IntStream.of();从静态方法中获取流。

Stream.generate();自己生成流

2.2.数据处理

数据处理/转换(intermedia)步骤可以有多个操作,这步也被称为intermedia(中间操作)。在这个步骤中不管怎样操作,它返回的都是一个新的流对象,原始数据不会发生任何改变,而且这个步骤是惰性计算处理的,也就是说只调用方法并不会开始处理,只有在真正的开始收集结果时,中间操作才会生效,而且如果遍历没有完成,想要的结果已经获取到了(比如获取第一个值),会停止遍历,然后返回结果。惰性计算可以显著提高运行效率。

数据处理演示。

TestpublicvoidstreamDemo(){ListStringnameList=Arrays.asList("Darcy","Chris","Linda","Sid","Kim","Jack","Poul","Peter");//1.筛选出名字长度为4的//2.名字前面拼接Thisis//3.遍历输出nameList.stream().filter(name-name.length()==4).map(name-"Thisis"+name).forEach(name-System.out.println(name));}//输出结果//ThisisJack//ThisisPoul

数据处理/转换操作自然不止是上面演示的过滤filter和map映射两种,另外还有map(mapToInt,flatMap等)、filter、distinct、sorted、peek、limit、skip、parallel、sequential、unordered等。

2.3.收集结果

结果处理(terminal)是流处理的最后一步,执行完这一步之后流会被彻底用尽,流也不能继续操作了。也只有到了这个操作的时候,流的数据处理/转换等中间过程才会开始计算,也就是上面所说的惰性计算。结果处理也必定是流操作的最后一步。

常见的结果处理操作有forEach、forEachOrdered、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst、findAny、iterator等。

下面演示了简单的结果处理的例子。

/***转换成为大写然后收集结果,遍历输出*/

TestpublicvoidtoUpperCaseDemo(){ListStringnameList=Arrays.asList("Darcy","Chris","Linda","Sid","Kim","Jack","Poul","Peter");ListStringupperCaseNameList=nameList.stream().map(String::toUpperCase).collect(Collectors.toList());upperCaseNameList.forEach(name-System.out.println(name+","));}//输出结果//DARCY,CHRIS,LINDA,SID,KIM,JACK,POUL,PETER,2.4.short-circuiting

有一种Stream操作被称作short-circuiting,它是指当Stream流无限大但是需要返回的Stream流是有限的时候,而又希望它能在有限的时间内计算出结果,那么这个操作就被称为short-circuiting。例如 findFirst 操作。

3.Stream流使用

Stream流在使用时候总是借助于Lambda表达式进行操作,Stream流的操作也有很多种方式,下面列举的是常用的11种操作。

3.1.Stream流获取

获取Stream的几种方式在上面的Stream数据源里已经介绍过了,下面是针对上面介绍的几种获取Stream流的使用示例。

TestpublicvoidcreateStream()throwsFileNotFoundException{ListStringnameList=Arrays.asList("Darcy","Chris","Linda","Sid","Kim","Jack","Poul","Peter");String[]nameArr={"Darcy","Chris","Linda","Sid","Kim","Jack","Poul","Peter"};//集合获取Stream流StreamStringnameListStream=nameList.stream();//集合获取并行Stream流StreamStringnameListStream2=nameList.parallelStream();//数组获取Stream流StreamStringnameArrStream=Stream.of(nameArr);//数组获取Stream流StreamStringnameArrStream1=Arrays.stream(nameArr);//文件流获取Stream流BufferedReaderbufferedReader=newBufferedReader(newFileReader("README.md"));StreamStringlinesStream=bufferedReader.lines();//从静态方法获取流操作IntStreamrangeStream=IntStream.range(1,10);rangeStream.limit(10).forEach(num-System.out.print(num+","));System.out.println();IntStreamintStream=IntStream.of(1,2,3,3,4);intStream.forEach(num-System.out.print(num+","));}3.2.forEach

forEach是Stream流中的一个重要方法,用于遍历Stream流,它支持传入一个标准的Lambda表达式。但是它的遍历不能通过return/break进行终止。同时它也是一个terminal操作,执行之后Stream流中的数据会被消费掉。

如输出对象。

ListIntegernumberList=Arrays.asList(1,2,3,4,5,6,7,8,9);numberList.stream().forEach(number-System.out.println(number+","));//输出结果//1,2,3,4,5,6,7,8,9,3.3.map/flatMap

使用map把对象一对一映射成另一种对象或者形式。

/***把数字值乘以2*/

TestpublicvoidmapTest(){ListIntegernumberList=Arrays.asList(1,2,3,4,5,6,7,8,9);//映射成2倍数字ListIntegercollect=numberList.stream().map(number-number*2).collect(Collectors.toList());collect.forEach(number-System.out.print(number+","));System.out.println();numberList.stream().map(number-"数字"+number+",").forEach(number-System.out.println(number));}//输出结果//2,4,6,8,10,12,14,16,18,//数字1,数字2,数字3,数字4,数字5,数字6,数字7,数字8,数字9,

上面的map可以把数据进行一对一的映射,而有些时候关系可能不止1对1那么简单,可能会有1对多。这时可以使用flatMap。下面演示使用flatMap把对象扁平化展开。

/***flatmap把对象扁平化*/

TestpublicvoidflatMapTest(){StreamListIntegerinputStream=Stream.of(Arrays.asList(1),Arrays.asList(2,3),Arrays.asList(4,5,6));ListIntegercollect=inputStream.flatMap((childList)-childList.stream()).collect(Collectors.toList());collect.forEach(number-System.out.print(number+","));}//输出结果//1,2,3,4,5,6,3.4.filter

使用filter进行数据筛选,挑选出想要的元素,下面的例子演示怎么挑选出偶数数字。

/***filter数据筛选*筛选出偶数数字*/

TestpublicvoidfilterTest(){ListIntegernumberList=Arrays.asList(1,2,3,4,5,6,7,8,9);ListIntegercollect=numberList.stream().filter(number-number%2==0).collect(Collectors.toList());collect.forEach(number-System.out.print(number+","));}

得到如下结果。

2,4,6,8,3.5.findFirst

findFirst可以查找出Stream流中的第一个元素,它返回的是一个Optional类型,如果还不知道Optional类的用处,可以参考之前文章Jdk14都要出了,还不能使用Optional优雅的处理空指针?。

/***查找第一个数据*返回的是一个Optional对象*/

TestpublicvoidfindFirstTest(){ListIntegernumberList=Arrays.asList(1,2,3,4,5,6,7,8,9);OptionalIntegerfirstNumber=numberList.stream().findFirst();System.out.println(firstNumber.orElse(-1));}//输出结果//1

findFirst方法在查找到需要的数据之后就会返回不再遍历数据了,也因此findFirst方法可以对有无限数据的Stream流进行操作,也可以说findFirst是一个short-circuiting操作。

3.6.collect/toArray

Stream流可以轻松的转换为其他结构,下面是几种常见的示例。

/***Stream转换为其他数据结构*/

TestpublicvoidcollectTest(){ListIntegernumberList=Arrays.asList(1,1,2,2,3,3,4,4,5);//toarrayInteger[]toArray=numberList.stream().toArray(Integer[]::new);//toListListIntegerintegerList=numberList.stream().collect(Collectors.toList());//tosetSetIntegerintegerSet=numberList.stream().collect(Collectors.toSet());System.out.println(integerSet);//tostringStringtoString=numberList.stream().map(number-String.valueOf(number)).collect(Collectors.joining()).toString();System.out.println(toString);//tostringsplitby,StringtoStringbJoin=numberList.stream().map(number-String.valueOf(number)).collect(Collectors.joining(",")).toString();System.out.println(toStringbJoin);}//输出结果//[1,2,3,4,5]////1,1,2,2,3,3,4,4,53.7.limit/skip

获取或者扔掉前n个元素

/***获取/扔掉前n个元素*/

TestpublicvoidlimitOrSkipTest(){//生成自己的随机数流ListIntegerageList=Arrays.asList(11,22,13,14,25,26);ageList.stream().limit(3).forEach(age-System.out.print(age+","));System.out.println();ageList.stream().skip(3).forEach(age-System.out.print(age+","));}//输出结果//11,22,13,//14,25,26,3.8.Statistics

数学统计功能,求一组数组的最大值、最小值、个数、数据和、平均数等。

/***数学计算测试*/

TestpublicvoidmathTest(){ListIntegerlist=Arrays.asList(1,2,3,4,5,6);IntSummaryStatisticsstats=list.stream().mapToInt(x-x).summaryStatistics();System.out.println("最小值:"+stats.getMin());System.out.println("最大值:"+stats.getMax());System.out.println("个数:"+stats.getCount());System.out.println("和:"+stats.getSum());System.out.println("平均数:"+stats.getAverage());}//输出结果//最小值:1//最大值:6//个数:6//和:21//平均数:3.53.9.groupingBy

分组聚合功能,和数据库的Groupby的功能一致。

/***groupingBy*按年龄分组*/

TestpublicvoidgroupByTest(){ListIntegerageList=Arrays.asList(11,22,13,14,25,26);MapString,ListIntegerageGrouyByMap=ageList.stream().collect(Collectors.groupingBy(age-String.valueOf(age/10)));ageGrouyByMap.forEach((k,v)-{System.out.println("年龄"+k+"0多岁的有:"+v);});}//输出结果//年龄10多岁的有:[11,13,14]//年龄20多岁的有:[22,25,26]3.10.partitioningBy

/***partitioningBy*按某个条件分组*给一组年龄,分出成年人和未成年人*/publicvoidpartitioningByTest(){ListIntegerageList=Arrays.asList(11,22,13,14,25,26);MapBoolean,ListIntegerageMap=ageList.stream().collect(Collectors.partitioningBy(age-age18));System.out.println("未成年人:"+ageMap.get(false));System.out.println("成年人:"+ageMap.get(true));}//输出结果//未成年人:[11,13,14]//成年人:[22,25,26]3.11.进阶-自己生成Stream流

/***生成自己的Stream流*/

TestpublicvoidgenerateTest(){//生成自己的随机数流Randomrandom=newRandom();StreamIntegergenerateRandom=Stream.generate(random::nextInt);generateRandom.limit(5).forEach(System.out::println);//生成自己的UUID流StreamUUIDgenerate=Stream.generate(UUID::randomUUID);generate.limit(5).forEach(System.out::println);}//输出结果////-//-////-//-a-4ad0-a-80aebd//faa-2f94--afcf-fbac//d86ccefe-1cd2-4eb4-bb0c-f2a//b-1df5-48f4--fa9c64c7e1c9//3af2a07f--f-a-6eeab3

上面的例子中Stream流是无限的,但是获取到的结果是有限的,使用了Limit限制获取的数量,所以这个操作也是short-circuiting操作。

4.Stream流优点4.1.简洁优雅

正确使用并且正确格式化的Stream流操作代码不仅简洁优雅,更让人赏心悦目。下面对比下在使用Stream流和不使用Stream流时相同操作的编码风格。

/***使用流操作和不使用流操作的编码风格对比*/

TestpublicvoiddiffTest(){//不使用流操作ListStringnames=Arrays.asList("Jack","Jill","Nate","Kara","Kim","Jullie","Paul","Peter");//筛选出长度为4的名字ListStringsubList=newArrayList();for(Stringname:names){if(name.length()==4){subList.add(name);}}//把值用逗号分隔StringBuildersbNames=newStringBuilder();for(inti=0;isubList.size()-1;i++){sbNames.append(subList.get(i));sbNames.append(",");}//去掉最后一个逗号if(subList.size()1){sbNames.append(subList.get(subList.size()-1));}System.out.println(sbNames);}//输出结果//Jack,Jill,Nate,Kara,Paul

如果是使用Stream流操作。

//使用Stream流操作StringnameString=names.stream().filter(num-num.length()==4).collect(Collectors.joining(","));System.out.println(nameString);4.2.惰性计算

上面有提到,数据处理/转换(intermedia)操作map(mapToInt,flatMap等)、filter、distinct、sorted、peek、limit、skip、parallel、sequential、unordered等这些操作,在调用方法时并不会立即调用,而是在真正使用的时候才会生效,这样可以让操作延迟到真正需要使用的时刻。

下面会举个例子演示这一点。

/***找出偶数*/

TestpublicvoidlazyTest(){//生成自己的随机数流ListIntegernumberLIst=Arrays.asList(1,2,3,4,5,6);//找出偶数StreamIntegerintegerStream=numberLIst.stream().filter(number-{inttemp=number%2;if(temp==0){System.out.println(number);}returntemp==0;});System.out.println("分割线");ListIntegercollect=integerStream.collect(Collectors.toList());}

如果没有惰性计算,那么很明显会先输出偶数,然后输出分割线。而实际的效果是。

分割线

可见惰性计算把计算延迟到了真正需要的时候。

4.3.并行计算

获取Stream流时可以使用parallelStream方法代替stream方法以获取并行处理流,并行处理可以充分的发挥多核优势,而且不增加编码的复杂性。

下面的代码演示了生成一千万个随机数后,把每个随机数乘以2然后求和时,串行计算和并行计算的耗时差异。

/***并行计算*/

Testpublicvoidmain(){//生成自己的随机数流,取一千万个随机数Randomrandom=newRandom();StreamIntegergenerateRandom=Stream.generate(random::nextInt);ListIntegernumberList=generateRandom.limit().collect(Collectors.toList());//串行-把一千万个随机数,每个随机数*2,然后求和longstart=System.currentTimeMillis();intsum=numberList.stream().map(number-number*2).mapToInt(x-x).sum();longend=System.currentTimeMillis();System.out.println("串行耗时:"+(end-start)+"ms,和是:"+sum);//并行-把一千万个随机数,每个随机数*2,然后求和start=System.currentTimeMillis();sum=numberList.parallelStream().map(number-number*2).mapToInt(x-x).sum();end=System.currentTimeMillis();System.out.println("并行耗时:"+(end-start)+"ms,和是:"+sum);}

得到如下输出。

串行耗时:ms,和是:并行耗时:47ms,和是:

效果显而易见,代码简洁优雅。

5.Stream流建议5.1保证正确排版

从上面的使用案例中,可以发现使用Stream流操作的代码非常简洁,而且可读性更高。但是如果不正确的排版,那么看起来将会很糟糕,比如下面的同样功能的代码例子,多几层操作呢,是不是有些让人头大?

//不排版Stringstring=names.stream().filter(num-num.length()==4).map(name-name.toUpperCase()).collect(Collectors.joining(","));//排版Stringstring=names.stream().filter(num-num.length()==4).map(name-name.toUpperCase()).collect(Collectors.joining(","));5.2保证函数纯度

如果想要你的Stream流对于每次的相同操作的结果都是相同的话,那么你必须保证Lambda表达式的纯度,也就是下面两点。

Lambda中不会更改任何元素。

Lambda中不依赖于任何可能更改的元素。

这两点对于保证函数的幂等非常重要,不然你程序执行结果可能会变得难以预测,就像下面的例子。

TestpublicvoidsimpleTest(){ListIntegernumbers=Arrays.asList(1,2,3);int[]factor=newint[]{2};StreamIntegerstream=numbers.stream().map(e-e*factor[0]);factor[0]=0;stream.forEach(System.out::println);}//输出结果//0//0//0

文中代码都已经上传到




转载请注明:http://www.92nongye.com/zyjs/zyjs/204623919.html

  • 上一篇文章:
  •   
  • 下一篇文章: 没有了