我终于搞懂了Java8 Stream流式编程,它竟然可以让代码变得简洁?

在实际项目当中,若能熟练使用Java8 的Stream流特性进行开发,就比较容易写出简洁优雅的代码。目前市面上很多开源框架,如Mybatis- Plus、kafka Streams以及Flink流处理等,都有一个相似的地方,即用到Stream流特性,其写出的代码简洁而易懂,当然,若是在不熟悉流特性的基础上而贸然去使用Stream开发的话,难免会写出一手bug。

我在项目当中,很早就开始使用Java 8的流特性进行开发了,但是一直都没有针对这块进行开发总结。这次就对这一块代码知识做一次全面总结,在总结的过程中去发现自己的不足,同时方便日后开发查询。

此文主要适合新手。

一、流(Stream)的概念

1.1、什么是流:流的概念、创建方式以及常见用途。

流(Stream)是对数据进行连续处理的抽象概念,可以看作数一种迭代器,按步骤处理数据元素。流的创建方式包括从集合、数组、文件等数据源获取输入流或者输出流,或者通过网络连接获取到网络流,例如Kafka 的流处理。常见的使用场景包括从大型数据源读取、过滤、数据转换、聚合等操作。

1.2、流的特性:流的惰性求值、短路操作、可消费性等特性。

惰性求值(Lazy Evaluation):流的元素只在需要时才进行计算,不会提前计算整个流,简而言之,就是延迟处理,可以一定程度上优化程序的性能。

短路操作(Short-Circuiting Operations):对于某些操作,如果前面的元素已经满足条件,后面的元素就不再需要进行处理,类似Java里的&&,例如,false&&true,前面第一个为false,后面的就无需再作判断了。

可消费性:流只能被消费一次,即每个元素只能被处理一次,就像河水一样,只能流过一次。

1.3、流的类型:了解基本类型流、对象类型流和无限流等不同类型的流。

根据数据类型和元素个数的不同,可以将流(Stream)分为以下几种类型:

基本类型流(Primitive Stream):处理基本数据类型,如IntStream、Long Stream和DoubleStream。

对象类型流(Object Stream):处理对象类型,如Stream,这里的T表示任意对象类型。

无限流(Infinite Stream):包含无限个元素的流,如Stream.iterate()和Stream.generate()方法生成的流。

并行流(Parallel Stream):将流划分成多个子流,充分利用多核处理器提高计算性能。

装饰流(Decorating Stream):通过对一个流进行装饰模式,实现流的增强功能,如排序、过滤、映射等操作。

二、中间操作

Stream的中间操作是指在流链当中,可以对数据进行处理操作,包括filter过滤、map映射转换、flatMap合并、distinct去重、sorted排序等操作。这些操作都会返回一个新的Stream流对象,可以通过链式调用多个中间操作进行复杂的数据处理。需要注意的是,中间操作需要具有终止操作才会触发。

下面按类别讲解Stream常见的中间操作。

2.1、filter:过滤出符合条件的元素。

filter()方法常用于实现数据过滤,即可以对集合、数组等数据源筛选出符合指定条件的元素,并返回一个新的流。

假设有一个黑名单手机号列表,需要筛选出其中所有开头为“133”的元素,那么可以通过filter()实现——

//将数组转换为一个字符串列表List numbers = Arrays.asList("13378520000","13278520000","13178520000","13358520000");//通过stream()方法创建一个流,接着使用filter()方法过滤出前缀为“133”的元素,最终通过collect() 方法将结果收集到一个新列表中List filterdNumbers = numbers.stream().filter(s -> s.startsWith("133")).collect(Collectors.toList());System.out.println(filterdNumbers);打印结果:[13378520000,13358520000]复制代码

2.2、map:映射转换元素。

map()方法用于对流中的每个元素进行映射操作,将其转换为另一个元素或者提取其中的信息,并返回一个新的流。

根据以下两个案例分别学习map()将元素转换为另一个元素以及提取元素其中的信息——

2.2.1、转换元素

假设有一个手机号字符列表,需要根据前7位来确定手机号归属地,那么就需要获取所有手机号前7位子字符串,可以使用map()方法实现:

List numbers = Arrays.asList("13378520000","13278520000","13178520000","13558520000");//通过stream()方法创建一个流,使用map()方法将每个字符串转换为截取前7位的字符,最后使用collect()方法将结果收集到一个新列表中List filterdNumbers = numbers.stream().map(s -> s.substring(0,7)).collect(Collectors.toList());System.out.println(filterdNumbers);打印结果:[1337852,1327852,1317852,1355852]复制代码

2.2.2、提取元素信息

假设有一个用户对象列表,我们需要提取其中每个对象的手机号,可以使用map()方法实现:

List peopleList = Arrays.asList(newPeople("王二","13378520000"),newPeople("李二","13278520000"),newPeople("张四","13178520000"));//通过stream()方法创建一个流,使用map()方法提取每个用户的手机号,最后使用collect()方法将结果收集到一个新列表中List tel = peopleList.stream().map(People::getTel).collect(Collectors.toList());System.out.println(tel);打印结果:[13378520000,13278520000,13178520000]复制代码

2.3、flatMap:将多个流合并为一个流。

flatMap()方法可以实现多对多的映射,或者将多个列表合并成一个列表操作。

2.3.1、实现多对多的映射

假设有两组余额列表A和B,需要将A组每个元素都与B组所有元素依次进行相加,可以使用flatMap实现该多对多的映射——

List<Integer>listA=Arrays.asList(1,2,3);List<Integer>listB=Arrays.asList(4,5,6);List<Integer>list=listA.stream().flatMap(a->listB.stream().map(b->a+b)).collect(Collectors.toList());System.out.println(list);打印结果:[5,6,7,6,7,8,7,8,9]复制代码

2.3.2、将多个列表合并成一个列表

假设有一个包含多个手机号字符串列表的列表,现在需要合并所有手机号字符串成为一个列表,可以使用flatMap()方法实现:

List> listOfLists = Arrays.asList(        Arrays.asList("13378520000","13278520000"),        Arrays.asList("13178520000","13558520000"),        Arrays.asList("15138510000","15228310000"));List flatMapList = listOfLists.stream().flatMap(Collection::stream).collect(Collectors.toList());System.out.println(flatMapList);打印结果:[13378520000,13278520000,13178520000,13558520000,15138510000,15228310000]复制代码

2.4、distinct:去除重复的元素。

distinct()方法可以用来去除流中的重复元素,生成无重复的列表。

假设有一个包含重复手机号字符串的列表,可以使用distinct()去重操作——

List numbers = Arrays.asList("13378520000","15138510000","13178520000","15138510000");List disNumbers = numbers.stream().distinct().collect(Collectors.toList());System.out.println(disNumbers);打印结果:[13378520000,15138510000,13178520000]复制代码

注意一点的是,distinct用于针对流作去重操作时,需要确定流中元素实现了equals()和hashCode()方法,因为这两个方法是判断两个对象是否相等的标准。

2.5、sorted:排序元素。

sorted()方法用于对流中的元素进行排序。

假设需要对一组People对象按照年龄排序,下面分别按照升序排序和降序排序——

2.5.1、升序排序

默认情况下,是升序排序——

List peopleList = Arrays.asList(newPeople("王二",20),newPeople("李二",30),newPeople("张四",31));List newpeopleList=peopleList.stream().sorted(Comparator.comparing(People::getAge)).collect(Collectors.toList());//打印结果newpeopleList.stream().forEach(System.out::println);打印结果:People{name='王二', age=20}People{name='李二', age=30}People{name='张四', age=31}复制代码

2.5.2、降序排序

通过reversed()方法进行逆序排序,也就是将升序排序进行倒序排序——

List peopleList = Arrays.asList(newPeople("王二",20),newPeople("李二",30),newPeople("张四",31));List newpeopleList = peopleList.stream().sorted(Comparator.comparing(People::getAge).reversed()).collect(Collectors.toList());//打印结果newpeopleList.stream().forEach(System.out::println);打印结果:People{name='张四', age=31}People{name='李二', age=30}People{name='王二', age=20}复制代码

2.6、peek:查看每个元素的信息,但不修改流中元素的状态。

peek()方法用于查看流中的元素而不会修改流中元素的状态,可以在流中的任何阶段使用,不会影响到流的操作,也不会终止流的操作。

List telList = Arrays.asList("13378520000","13278520000","13178520000","13558520000");telList.stream().peek(t -> System.out.println(t))        .map(t -> t.substring(0,3))        .peek(t -> System.out.println(t))        .collect(Collectors.toList());打印结果:1337852000013313278520000132复制代码

peek()方法和forEach很类似,都是可以用于遍历流中的元素,但是,两者之间存在较大的区别。主要一点是,forEach在流中是一个终止操作,一旦调用它,就意味着Stream流已经被处理完成,不能再进行任何操作,例如,无法在forEach之后针对流进行map、filter等操作,但peek方法可以,以上的案例可以看出,在第一次调用peek打印一个元素后,该元素还可以接着进行map操作,进行字符串的前三位截取。

这是peek()方法和forEach最大的区别。

2.7、limit 和 skip:截取流中的部分元素。

limit()和skip()都是用于截取Stream流中部分元素的方法,两者区别在于,limit()返回一个包含前n个元素的新流,skip()则返回一个丢弃前n个元素后剩余元素组成的新流。

int[]arr={1,2,3,4,5,6,7,8,9,10};System.out.print("取数组前5个元素:");Arrays.stream(arr).limit(5).forEach(n->System.out.print(n+" "));//输出结果为:12345System.out.print("跳过前3个元素,取剩余数组元素:");Arrays.stream(arr).skip(3).forEach(n->System.out.print(n+" "));//输出结果为:45678910复制代码

三、终止操作

Stream的终止操作是指执行Stream流链中最后一个步骤,到这一步就会结束整个流处理。在Java8中,Stream终止操作包括forEach、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst和findAny等。这些终止操作都有返回值。需要注意一点是,如果没有执行终止操作的话,Stream流是不会触发执行的,例如,一个没有终止操作的peek()方法代码是不会执行进而打印——

list.stream().peek(t->System.out.println("ddd"))复制代码

当加上终止操作话,例如加上collect,就会打印出“ddd”——

list.stream().peek(t->System.out.println("ddd")).collect(Collectors.toList());复制代码

下面按类别分别讲解各个终止操作的使用。

3.1、forEach:遍历流中的每个元素。

该forEach前面已经提到,这里不做过多介绍。

3.2、count:统计流中元素的数量。

count可以统计流中元素的数量并返回结果。

假设有一个包含多个手机号字符串的列表,需要统计去重后的手机号数量,就可以使用count方法——

List numbers =Arrays.asList("13378520000","15138510000","13178520000","15138510000");longcount= numbers.stream()        .distinct()//去重.count();//统计去重后的手机号System.out.println(count);打印结果:3复制代码

3.3、reduce:将流中的所有元素归约成一个结果。

reduce()可以将流中的所有元素根据指定规则归约成一个结果,并将该结果返回。

常用语法格式如下:

Optionalresult = stream.reduce(BinaryOperatoraccumulator);复制代码

可见,reduce方法会返回一个Optional类型的值,表示归约后的结果,需要通过get()方法获取Optional里的值。

假设有一个包含多个手机号字符串的List列表,需要在去重之后,再将列表所有字符串拼按照逗号间隔接成一个字符串返回,那么就可以通过reduce来实现——

List numbers =Arrays.asList("13378520000","15138510000","13178520000","15138510000");Optionalresult = numbers.stream()        .distinct()//去重.reduce((a ,b) -> a+","+b);//指定规则为,相临两个字符通过逗号“,”间隔System.out.println(result.get());打印结果:13378520000,15138510000,13178520000复制代码

3.4、collect:将流中的元素收集到一个容器中,并返回该容器。

collect的作用是将流中的元素收集到一个新的容器中,返回该容器。打个比喻,它就像一个采摘水果的工人,负责将水果一个个采摘下来,然后放进一个篮子里,最后将篮子交给你。我在前面的案例当中,基本都有用到collect,例如前面2.1的filter过滤用法中的List filterdNumbers = numbers.stream().filter(s -> s.startsWith("133")).collect(Collectors.toList()),就是将过滤出前缀为“133”的字符串,将这些过滤处理后的元素交给collect这个终止操作。这时collect就像采摘水果的员工,把采摘为前缀“133”的“水果”通过toList()方法收集到一个新的List容器当中,然后交给你。最后你就可以得到一个只装着前缀为“133”的元素集合。

在Java8的collect方法中,除里toList()之外,还提供了例如toSet,toMap等方法满足不同的场景,根据名字就可以知道,toSet()返回的是一个Set集合,toMap()返回的是一个Map集合。

3.5、min 和 max:找出流中的最小值和最大值。

min和max用来查找流中的最小值和最大值。

假设需要在查找出用户列表中年龄最小的用户,可以按照以下代码实现——

List peopleList = Arrays.asList(newPeople("王二",20),newPeople("李二",30),newPeople("张四",31));//查找年龄最小的用户,若没有则返回一个nullPeople people = peopleList.stream().min(Comparator.comparing(People::getAge)).orElse(null);System.out.println(people);打印结果:People{name='王二', age=20}复制代码

max的用法类似,这里不做额外说明。

3.6、anyMatch、allMatch 和 noneMatch:判断流中是否存在满足指定条件的元素。

3.6.1、anyMatch

anyMatch用于判断,如果流中至少有一个元素满足给定条件,那么返回true,反之返回false,即 true||false为true这类的判断。

假设在一个手机号字符串的List列表当中,判断是否包含前缀为“153”的手机号,就可以使用anyMatch——

List numbers = Arrays.asList("13378520000","15138510000","13178520000","15338510000");boolean hasNum = numbers.stream().anyMatch(n -> n.startsWith("153"));System.out.println(hasNum);打印结果:true复制代码

3.6.2、allMatch

allMatch用于判断,流中的所有元素是否都满足给定条件,满足返回true,反之false,即true&&false为false这类判断。

假设在一个手机号字符串的List列表当中,判断手机号是否都满足前缀为“153”的手机号,就可以用allMatch——

List numbers = Arrays.asList("13378520000","15138510000","13178520000","15338510000");boolean hasNum = numbers.stream().allMatch(n -> n.startsWith("153"));System.out.println(hasNum);打印结果:false复制代码

3.6.2、noneMatch

noneMatch用于判断,如果流中没有任何元素满足给定的条件,返回true,如果流中有任意一个条件满足给定条件,返回false,类似!true为false的判断。

假设在一个手机号字符串的List列表当中,判断手机号是否都不满足前缀为“153”的手机号,就可以用noneMatch——

List numbers = Arrays.asList("13378520000","15138510000","13178520000","1238510000");//numbers里没有前缀为“153”的手机号boolean hasNum = numbers.stream().noneMatch(n -> n.startsWith("153"));System.out.println(hasNum);打印结果:true复制代码

这三个方法其实存在一定互相替代性,例如在3.6.1中,满足!anyMatch表示所有手机号都不为“153”前缀,才得到true,这不就是noneMatch,主要看在项目当中如何灵活应用。

3.7、findFirst 和 findAny:返回流中第一个或任意一个元素。

3.7.1、findFirst

findFirst用于返回流中第一个元素,如果流为空话,则返回一个空的Optional对象——

假设需要对一批同手机号的黑名单用户按照时间戳降序排序,然后取出第一个即时间戳为最早的用户,就可以使用findFirst——

List peopleList = Arrays.asList(        new People("王二","13178520000","20210409"),        new People("李二","13178520000","20230401"),        new People("张四","13178520000","20220509"),        new People("赵六","13178520000","20220109"));/** * 先按照时间升序排序,排序后的结果如下: *  People{name='王二', tel='13178520000',time='20210409'} *  People{name='赵六', tel='13178520000',time='20220109'} *  People{name='张四', tel='13178520000',time='20220509'} *  People{name='李二', tel='13178520000',time='20230401'} * *排序后,People{name='王二', tel='13178520000',time='20210409'}成了流中的第一个元素 */People people = peopleList.stream().sorted(Comparator.comparing(People::getTime)).findFirst().orElse(null);System.out.println(people);打印结果:People{name='王二', tel='13178520000',time='20210409'} 复制代码

3.7.2、findAny

findAny返回流中的任意一个元素,如果流为空,则通过Optional对象返回一个null。

假设有一个已经存在的黑名单手机号列表blackList,现在有一批新的手机号列表phoneNumber,需要基于blackList列表过滤出phoneNumber存在的黑名单手机号,最后从过滤出来的黑名单手机号当中挑选出来出来任意一个,即可以通过findAny实现——

//blackList是已经存在的黑名单列表List blackList = Arrays.asList("13378520000","15138510000");//新来的手机号列表List phoneNumber = Arrays.asList("13378520000","13178520000","1238510000","15138510000","13299920000");StringblackPhone = phoneNumber.stream()//过滤出phoneNumber有包含在blackList的手机号,这类手机号即为黑名单手机号。.filter(phone -> blackList.contains(phone))//获取过滤确定为黑名单手机号的任意一个.findAny()//如果没有则返回一个null.orElse(null);System.out.println(blackPhone);打印结果:13378520000复制代码

四、并行流

前面的案例主要都是以顺序流来讲解,接下来,就是讲解Stream的并行流。在大数据量处理场景下,使用并行流可以提高某些操作效率,但同样存在一些需要考虑的问题,并非所有情况下都可以使用。

4.1、什么是并行流:并行流的概念和原理。

并行流是指通过将数据按照一定的方式划分成多个片段分别在多个处理器上并行执行,这就意味着,可能处理完成的数据顺序与原先排序好的数据情况是不一致的。主要是用在比较大的数据量处理情况,若数据量太少,效率并不比顺序流要高,因为底层其实就使用到了多线程的技术。

并行流的流程原理如下:

**1、输入数据:**并行流的初始数据一般是集合或者数组,例如Arrays.asList("13378520000", "13178520000", "1238510000","15138510000","13299920000");

**2、划分数据:**将初始数据平均分成若干个子集,每个子集可以在不同的线程中独立进行处理,这个过程通常叫“分支”(Forking),默认情况下,Java8并行流使用到了ForkJoinPool框架,会将Arrays.asList("13378520000", "13178520000", "1238510000","15138510000","13299920000")划分成更小的颗粒进行处理,可能会将该数组划分成以下三个子集:

[13378520000, 13178520000][1238510000, 13338510000][13299920000]复制代码

**3、处理数据:**针对划分好的子集并行进行相同的操作,例如包括过滤(filter)、映射(map)、去重(distinct)等,这个过程通常叫“计算”(Computing),例如需要过滤为前缀包括“133”的字符集合,那么,各个子集,就会处理得到以下结果:

[13378520000][13338510000][]复制代码

**4、合并结果:**将所有子集处理完成的结果进行汇总,得到最终结果。这个过程通常叫“合并”(Merging),结果就会合并如下:

[13378520000,13338510000]  复制代码

**5、返回结果:**返回最终结果。

通俗而言,就是顺序流中,只有一个工人在摘水果,并行流中,是多个工人同时在摘水果。

4.2、创建并行流:通过 parallel() 方法将串行流转换为并行流。

可以通过parallel()方法将顺序流转换为并行流,操作很简单,只需要在顺序流上调用parallel()即可。

List numbers = Arrays.asList("13378360000","13278240000","13178590000","13558120000");//通过stream().parallel()方法创建一个并行流,使用map()方法将每个字符串转换为截取前7位的字符,最后使用collect()方法将结果收集到一个新列表中List filNums = numbers.stream().parallel().map(s -> s.substring(0,7)).collect(Collectors.toList());System.out.println(filNums);打印结果:[1337836,1327824,1317859,1355812]复制代码

4.3、并行流的注意事项:并行流可能引发的线程安全,以及如何避免这些问题。

在使用并发流的过程中,可能会引发以下线程安全问题:并行流中的每个子集都在不同线程运行,可能会导致对共享状态的竞争和冲突。

避免线程问题的方法如下:避免修改共享状态,即在处理集合过程当中,避免被其他线程修改集合数据,可以使用锁来保证线程安全。

使用无状态操作:在并行流处理过程尽量使用无状态操作,例如filter、map之类的,可以尽量避免线程安全和同步问题。

五、Optional

5.1、什么是 Optional:Optional 类型的作用和使用场景。

在实际开发当中,Optional类型通常用于返回可能为空的方法、避免null值的传递和简化复杂的判断逻辑等场景。调用Optional对象的方法,需要通过isPresent()方法判断值是否存在,如果存在则可以通过get()方法获取其值,如果不存在则可以通过orElse()方法提供默认值,或者抛出自定义异常处理。

5.2、如何使用 Optional:如何使用 Optional 类型。

使用Optional类型主要目的是在数据可能为空的情况下,提供一种更安全、更优雅的处理方式。

以下是使用Optional类型的常用方法:

5.2.1、ofNullable()和isPresent()方法

将一个可能为null的对象包装成Optional类型的对象,然后根据isPresent方法判断对象是否包含空值——

Stringstr= null;Optional optStr = Optional.ofNullable(str);if(optStr.isPresent()){    System.out.println("Optional对象不为空");}else{    System.out.println("Optional对象为空");}打印结果:Optional对象为空复制代码

5.2.2、get()方法

获取Optional对象中的值,如果对象为空则抛出NoSuchElementException异常——

String str =null;Optional optStr = Optional.ofNullable(str);if(optStr.isPresent()){    System.out.println("Optional对象不为空");}else{    System.out.println("Optional对象为空");    optStr.get();}控制台打印结果:Exceptioninthread"main"java.util.NoSuchElementException: Novaluepresentat java.util.Optional.get(Optional.java:135)at com.zhu.fte.biz.test.StreamTest.main(StreamTest.java:144)Optional对象为空复制代码

5.2.4、orElse()方法

获取Optional对象中的值,如果对象为空则返回指定的默认值——

String str =null;Optional optStr = Optional.ofNullable(str);if(optStr.isPresent()){    System.out.println("Optional对象不为空");}else{    System.out.println("Optional对象为空,返回默认值:"+ optStr.orElse("null"));}打印结果:Optional对象为空,返回默认值:null复制代码

当然,如果不为空的话,则能正常获取对象中的值——

Stringstr="测试";Optional optStr = Optional.ofNullable(str);if(optStr.isPresent()){    System.out.println("Optional对象不为空,返回值:"+ optStr.orElse("null"));}else{    System.out.println("Optional对象为空,返回默认值:"+ optStr.orElse("null"));}打印结果:Optional对象不为空,返回值:测试复制代码

那么,问题来了,它是否能判断“ ”这类空格的字符串呢,我实验了一下,

Stringstr="    ";Optional optStr = Optional.ofNullable(str);if(optStr.isPresent()){    System.out.println("Optional对象不为空,返回值:"+ optStr.orElse("null"));}else{    System.out.println("Optional对象为空,返回默认值:"+ optStr.orElse("null"));}打印结果:Optional对象不为空,返回值:复制代码

可见,这类空字符串,在orElse判断当中,跟StringUtils.isEmpty()类似,都是把它当成非空字符串,但是StringUtils.isBlank()则判断为空字符串。

5.2.5、orElseGet()方法

orElseGet()和orElse()类似,都可以提供一个默认值。两者区别在于,orElse方法在每次调用时都会创建默认值,而orElseGet只在需要时才会创建默认值。

5.3、Optional 和 null 的区别: Optional 类型与 null 值的异同。

两者都可以表示缺失值的情况,两者主要区别为:Optional类型是一种包装器对象,可以将一个可能为空的对象包装成一个Optional对象。这个对象可以通过调用ofNullable()、of()或其他方法来创建。而null值则只是一个空引用,没有任何实际的值。

Optional类型还可以避免出现NullPointerException异常,具体代码案例如下:

String str =null;//错误示范:直接调用str.length()方法会触发NullPointerException//int length = str.length()//通过Optional类型避免NullPointerExceptionOptional optionalStr = Optional.ofNullable(str);if(optionalStr.isPresent()){//判断Optional对象是否都包含非空值intlength = optionalStr.get().length();    System.out.println("字符串长度为:"+ length);}else{    System.out.println("字符串为空!");}//使用map()方法对Optional对象进行转换时,确保返回对结果不为nullOptional optionalLength = optionalStr.map(s -> s.length());System.out.println("字符串长度为:"+ optionalLength.orElse(-1));// 使用orElse()方法提供默认值复制代码

六、扩展流处理

除里以上常用的流处理之外,Java8还新增了一些专门用来处理基本类型的流,例如IntStream、LongStream、DoubleStream等,其对应的Api接口基本与前面案例相似,读者可以自行研究。

最后,需要注意一点是,在流处理过程当中,尽量使用原始类型数据,避免装箱操作,因为装箱过程会有性能开销、内存占用等问题,例如,当原始数据int类型被装箱成Integer包装类型时,这个过程会涉及到对象的创建、初始化、垃圾回收等过程,需要额外的性能开销。

以上,就是关于Java8流处理相关知识的总结,笔者水平有限,若存在有误的地方,还需帮忙指正。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容