还看不懂同事的代码?超强的 Stream 流操作姿势还不学习一下

2022-10-13,,,,

java 8 新特性系列文章索引。

  1. jdk14都要出了,还不能使用 optional优雅的处理空指针?
  2. jdk14 都要出了,jdk8 的时间处理姿势还不了解一下?
  3. 还看不懂同事的代码?lambda 表达式、函数接口了解一下

前言

我们都知道 lambda 和 stream 是 java 8 的两大亮点功能,在前面的文章里已经介绍过 lambda 相关知识,这次介绍下 java 8 的 stream 流操作。它完全不同于 java.io 包的 input/output stream ,也不是大数据实时处理的 stream 流。这个 stream 流操作是 java 8 对集合操作功能的增强,专注于对集合的各种高效、便利、优雅的聚合操作。借助于 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(t array) or stream.of(); 从数组获取流。
  • bufferedreader.lines(); 从输入流中获取流。
  • intstream.of() ; 从静态方法中获取流。
  • stream.generate(); 自己生成流

2.2. 数据处理

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

数据处理演示。

@test
public void streamdemo(){
    list<string> namelist = arrays.aslist("darcy", "chris", "linda", "sid", "kim", "jack", "poul", "peter");
    // 1. 筛选出名字长度为4的
    // 2. 名字前面拼接 this is
    // 3. 遍历输出
    namelist.stream()
            .filter(name -> name.length() == 4)
            .map(name -> "this is "+name)
            .foreach(name -> system.out.println(name));
}
// 输出结果
// this is jack
// this is poul

数据处理/转换操作自然不止是上面演示的过滤 filtermap映射两种,另外还有 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 等。

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

/**
 * 转换成为大写然后收集结果,遍历输出
 */
@test
public void touppercasedemo() {
    list<string> namelist = arrays.aslist("darcy", "chris", "linda", "sid", "kim", "jack", "poul", "peter");
    list<string> uppercasenamelist = 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 流的使用示例。

@test
public void createstream() throws filenotfoundexception {
    list<string> namelist = arrays.aslist("darcy", "chris", "linda", "sid", "kim", "jack", "poul", "peter");
    string[] namearr = {"darcy", "chris", "linda", "sid", "kim", "jack", "poul", "peter"};
    // 集合获取 stream 流
    stream<string> nameliststream = namelist.stream();
    // 集合获取并行 stream 流
    stream<string> nameliststream2 = namelist.parallelstream();
    // 数组获取 stream 流
    stream<string> namearrstream = stream.of(namearr);
    // 数组获取 stream 流
    stream<string> namearrstream1 = arrays.stream(namearr);
    // 文件流获取 stream 流
    bufferedreader bufferedreader = new bufferedreader(new filereader("readme.md"));
    stream<string> linesstream = bufferedreader.lines();
    // 从静态方法获取流操作
    intstream rangestream = intstream.range(1, 10);
    rangestream.limit(10).foreach(num -> system.out.print(num+","));
    system.out.println();
    intstream intstream = intstream.of(1, 2, 3, 3, 4);
    intstream.foreach(num -> system.out.print(num+","));
}

3.2. foreach

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

如输出对象。

list<integer> numberlist = 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
 */
@test
public void maptest() {
    list<integer> numberlist = arrays.aslist(1, 2, 3, 4, 5, 6, 7, 8, 9);
    // 映射成 2倍数字
    list<integer> collect = 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把对象扁平化
 */
@test
public void flatmaptest() {
    stream<list<integer>> inputstream = stream.of(
            arrays.aslist(1),
            arrays.aslist(2, 3),
            arrays.aslist(4, 5, 6)
    );
    list<integer> collect = 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 数据筛选
 * 筛选出偶数数字
 */
@test
public void filtertest() {
    list<integer> numberlist = arrays.aslist(1, 2, 3, 4, 5, 6, 7, 8, 9);
    list<integer> collect = 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 对象
 */
@test
public void findfirsttest(){
    list<integer> numberlist = arrays.aslist(1, 2, 3, 4, 5, 6, 7, 8, 9);
    optional<integer> firstnumber = numberlist.stream()
            .findfirst();
    system.out.println(firstnumber.orelse(-1));
}
// 输出结果
// 1

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

3.6. collect / toarray

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

 /**
 * stream 转换为其他数据结构
 */
@test
public void collecttest() {
    list<integer> numberlist = arrays.aslist(1, 1, 2, 2, 3, 3, 4, 4, 5);
    // to array
    integer[] toarray = numberlist.stream()
            .toarray(integer[]::new);
    // to list
    list<integer> integerlist = numberlist.stream()
            .collect(collectors.tolist());
    // to set
    set<integer> integerset = numberlist.stream()
            .collect(collectors.toset());
    system.out.println(integerset);
    // to string
    string tostring = numberlist.stream()
            .map(number -> string.valueof(number))
            .collect(collectors.joining()).tostring();
    system.out.println(tostring);
    // to string split by ,
    string tostringbjoin = numberlist.stream()
            .map(number -> string.valueof(number))
            .collect(collectors.joining(",")).tostring();
    system.out.println(tostringbjoin);
}
// 输出结果
// [1, 2, 3, 4, 5]
// 112233445
// 1,1,2,2,3,3,4,4,5

3.7. limit / skip

获取或者扔掉前 n 个元素

/**
 * 获取 / 扔掉前 n 个元素
 */
@test
public void limitorskiptest() {
    // 生成自己的随机数流
    list<integer> agelist = 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

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

/**
 * 数学计算测试
 */
@test
public void mathtest() {
    list<integer> list = arrays.aslist(1, 2, 3, 4, 5, 6);
    intsummarystatistics stats = 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.5

3.9. groupingby

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

/**
 * groupingby
 * 按年龄分组
 */
@test
public void groupbytest() {
    list<integer> agelist = arrays.aslist(11, 22, 13, 14, 25, 26);
    map<string, list<integer>> agegrouybymap = 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
 * 按某个条件分组
 * 给一组年龄,分出成年人和未成年人
 */
public void partitioningbytest() {
    list<integer> agelist = arrays.aslist(11, 22, 13, 14, 25, 26);
    map<boolean, list<integer>> agemap = agelist.stream()
            .collect(collectors.partitioningby(age -> age > 18));
    system.out.println("未成年人:" + agemap.get(false));
    system.out.println("成年人:" + agemap.get(true));
}
// 输出结果
// 未成年人:[11, 13, 14]
// 成年人:[22, 25, 26]

3.11. 进阶 - 自己生成 stream 流

/**
 * 生成自己的 stream 流
 */
@test
public void generatetest(){
    // 生成自己的随机数流
    random random = new random();
    stream<integer> generaterandom = stream.generate(random::nextint);
    generaterandom.limit(5).foreach(system.out::println);
    // 生成自己的 uuid 流
    stream<uuid> generate = stream.generate(uuid::randomuuid);
    generate.limit(5).foreach(system.out::println);
}

// 输出结果
// 793776932
// -2051545609
// -917435897
// 298077102
// -1626306315
// 31277974-841a-4ad0-a809-80ae105228bd
// f14918aa-2f94-4774-afcf-fba08250674c
// d86ccefe-1cd2-4eb4-bb0c-74858f2a7864
// 4905724b-1df5-48f4-9948-fa9c64c7e1c9
// 3af2a07f-0855-455f-a339-6e890e533ab3

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

4. stream 流优点

4.1. 简洁优雅

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

/**
 * 使用流操作和不使用流操作的编码风格对比
 */
@test
public void difftest() {
    // 不使用流操作
    list<string> names = arrays.aslist("jack", "jill", "nate", "kara", "kim", "jullie", "paul", "peter");
    // 筛选出长度为4的名字
    list<string> sublist = new arraylist<>();
    for (string name : names) {
        if (name.length() == 4) {
            sublist.add(name);
        }
    }
    // 把值用逗号分隔
    stringbuilder sbnames = new stringbuilder();
    for (int i = 0; i < sublist.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 流操作
string namestring = 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 等这些操作,在调用方法时并不会立即调用,而是在真正使用的时候才会生效,这样可以让操作延迟到真正需要使用的时刻。

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

 /**
  * 找出偶数
  */
 @test
 public void lazytest() {
     // 生成自己的随机数流
     list<integer> numberlist = arrays.aslist(1, 2, 3, 4, 5, 6);
     // 找出偶数
     stream<integer> integerstream = numberlist.stream()
             .filter(number -> {
                 int temp = number % 2;
                 if (temp == 0 ){
                     system.out.println(number);
                 }
                 return temp == 0;
             });

     system.out.println("分割线");
     list<integer> collect = integerstream.collect(collectors.tolist());
 }

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

分割线
2
4
6

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

4.3. 并行计算

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

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

  /**
  * 并行计算
  */
 @test
 public void main() {
     // 生成自己的随机数流,取一千万个随机数
     random random = new random();
     stream<integer> generaterandom = stream.generate(random::nextint);
     list<integer> numberlist = generaterandom.limit(10000000).collect(collectors.tolist());

     // 串行 - 把一千万个随机数,每个随机数 * 2 ,然后求和
     long start = system.currenttimemillis();
     int sum = numberlist.stream()
         .map(number -> number * 2)
         .maptoint(x -> x)
         .sum();
     long end = 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);
 }

得到如下输出。

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

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

5. stream 流建议

5.1 保证正确排版

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

// 不排版
string string = names.stream().filter(num -> num.length() == 4).map(name -> name.touppercase()).collect(collectors.joining(","));
// 排版
string string = names.stream()
        .filter(num -> num.length() == 4)
        .map(name -> name.touppercase())
        .collect(collectors.joining(","));

5.1 保证函数纯度

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

  • lambda 中不会更改任何元素。
  • lambda 中不依赖于任何可能更改的元素。

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

@test
public void simpletest(){
    list<integer> numbers = arrays.aslist(1, 2, 3);
    int[] factor = new int[] { 2 };
    stream<integer> stream = numbers.stream()
            .map(e -> e * factor[0]);
    factor[0] = 0;
    stream.foreach(system.out::println);
}
// 输出结果
// 0
// 0
// 0

文中代码都已经上传到

https://github.com/niumoo/jdk-feature/blob/master/src/main/java/net/codingme/feature/jdk8/jdk8stream.java。

个人网站:
如果你喜欢这篇文章,可以关注公众号,一起成长。
关注公众号回复资源可以没有套路的获取全网最火的的 java 核心知识整理&面试资料。

《还看不懂同事的代码?超强的 Stream 流操作姿势还不学习一下.doc》

下载本文的Word格式文档,以方便收藏与打印。