关于后端:全面吃透JAVA-Stream流操作让代码更加的优雅

31次阅读

共计 9977 个字符,预计需要花费 25 分钟才能阅读完成。

在 JAVA 中,波及到对 数组 Collection 等汇合类中的元素进行操作的时候,通常会通过 循环的形式 进行一一解决,或者 应用 Stream的形式进行解决。

例如,当初有这么一个需要:

从给定句子中返回单词长度大于 5 的单词列表,按长度倒序输入,最多返回 3 个

JAVA7 及之前 的代码中,咱们会能够照如下的形式进行实现:


/**
 *【惯例形式】* 从给定句子中返回单词长度大于 5 的单词列表,按长度倒序输入,最多返回 3 个
 *
 * @param sentence 给定的句子,约定非空,且单词之间仅由一个空格分隔
 * @return 倒序输入符合条件的单词列表
 */
public List<String> sortGetTop3LongWords(@NotNull String sentence) {
    // 先切割句子,获取具体的单词信息
    String[] words = sentence.split(" ");
    List<String> wordList = new ArrayList<>();
    // 循环判断单词的长度,先过滤出合乎长度要求的单词
    for (String word : words) {if (word.length() > 5) {wordList.add(word);
        }
    }
    // 对符合条件的列表依照长度进行排序
    wordList.sort((o1, o2) -> o2.length() - o1.length());
    // 判断 list 后果长度,如果大于 3 则截取前三个数据的子 list 返回
    if (wordList.size() > 3) {wordList = wordList.subList(0, 3);
    }
    return wordList;
}

JAVA8 及之后 的版本中,借助 Stream 流,咱们能够更加优雅的写出如下代码:


/**
 *【Stream 形式】* 从给定句子中返回单词长度大于 5 的单词列表,按长度倒序输入,最多返回 3 个
 *
 * @param sentence 给定的句子,约定非空,且单词之间仅由一个空格分隔
 * @return 倒序输入符合条件的单词列表
 */
public List<String> sortGetTop3LongWordsByStream(@NotNull String sentence) {return Arrays.stream(sentence.split(" "))
            .filter(word -> word.length() > 5)
            .sorted((o1, o2) -> o2.length() - o1.length())
            .limit(3)
            .collect(Collectors.toList());
}

直观感触上,Stream的实现形式代码更加简洁、零打碎敲。很多的同学在代码中也常常应用 Stream 流,然而对 Stream 流的认知往往也是仅限于会一些简略的 filtermapcollect等操作,但 JAVA 的 Stream 能够实用的场景与能力远不止这些。

那么问题来了:Stream 相较于传统的 foreach 的形式解决 stream,到底有啥劣势

这里咱们能够先搁置这个问题,先整体全面的理解下 Stream,而后再来探讨下这个问题。

笔者联合在团队中多年的代码检视遇到的状况,联合平时我的项目编码实践经验,对 Stream 的外围要点与易混同用法 典型应用场景 等进行了具体的梳理总结,心愿能够帮忙大家对 Stream 有个更全面的认知,也能够更加高效的利用到我的项目开发中去。

Stream 初相识

概括讲,能够将 Stream 流操作分为 3 种类型

  • 创立 Stream
  • Stream 两头解决
  • 终止 Steam

每个 Stream 管道操作类型都蕴含若干 API 办法,先列举下各个 API 办法的性能介绍。

  • 开始管道

次要负责新建一个 Stream 流,或者基于现有的数组、List、Set、Map 等汇合类型对象创立出新的 Stream 流。

API 性能阐明
stream() 创立出一个新的 stream 串行流对象
parallelStream() 创立出一个可并行执行的 stream 流对象
Stream.of() 通过给定的一系列元素创立一个新的 Stream 串行流对象
  • 两头管道

负责对 Stream 进行解决操作,并返回一个新的 Stream 对象,两头管道操作能够进行 叠加

API 性能阐明
filter() 依照条件过滤符合要求的元素,返回新的 stream 流
map() 将已有元素转换为另一个对象类型,一对一逻辑,返回新的 stream 流
flatMap() 将已有元素转换为另一个对象类型,一对多逻辑,即原来一个元素对象可能会转换为 1 个或者多个新类型的元素,返回新的 stream 流
limit() 仅保留汇合后面指定个数的元素,返回新的 stream 流
skip() 跳过汇合后面指定个数的元素,返回新的 stream 流
concat() 将两个流的数据合并起来为 1 个新的流,返回新的 stream 流
distinct() 对 Stream 中所有元素进行去重,返回新的 stream 流
sorted() 对 stream 中所有的元素依照指定规定进行排序,返回新的 stream 流
peek() 对 stream 流中的每个元素进行一一遍历解决,返回解决后的 stream 流
  • 终止管道

顾名思义,通过终止管道操作之后,Stream 流将 会完结,最初可能会执行某些逻辑解决,或者是依照要求返回某些执行后的后果数据。

API 性能阐明
count() 返回 stream 解决后最终的元素个数
max() 返回 stream 解决后的元素最大值
min() 返回 stream 解决后的元素最小值
findFirst() 找到第一个符合条件的元素时则终止流解决
findAny() 找到任何一个符合条件的元素时则退出流解决,这个 对于串行流时与 findFirst 雷同,对于并行流时比拟高效,任何分片中找到都会终止后续计算逻辑
anyMatch() 返回一个 boolean 值,相似于 isContains(), 用于判断是否有符合条件的元素
allMatch() 返回一个 boolean 值,用于判断是否所有元素都符合条件
noneMatch() 返回一个 boolean 值,用于判断是否所有元素都不符合条件
collect() 将流转换为指定的类型,通过 Collectors 进行指定
toArray() 将流转换为数组
iterator() 将流转换为 Iterator 对象
foreach() 无返回值,对元素进行一一遍历,而后执行给定的解决逻辑

Stream 办法应用

map 与 flatMap

mapflatMap都是用于转换已有的元素为其它元素,区别点在于:

  • map 必须是一对一的,即每个元素都只能转换为 1 个新的元素
  • flatMap 能够是一对多的,即每个元素都能够转换为 1 个或者多个新的元素

比方:有一个字符串 ID 列表,当初须要将其转为 User 对象列表。能够应用 map 来实现:


/**
 * 演示 map 的用处:一对一转换
 */
public void stringToIntMap() {List<String> ids = Arrays.asList("205", "105", "308", "469", "627", "193", "111");
    // 应用流操作
    List<User> results = ids.stream()
            .map(id -> {User user = new User();
                user.setId(id);
                return user;
            })
            .collect(Collectors.toList());
    System.out.println(results);
}

执行之后,会发现每一个元素都被转换为对应新的元素,然而前后总元素个数是统一的:


[User{id='205'}, 
 User{id='105'},
 User{id='308'}, 
 User{id='469'}, 
 User{id='627'}, 
 User{id='193'}, 
 User{id='111'}]

再比方:现有一个句子列表,须要将句子中每个单词都提取进去失去一个所有单词列表 。这种状况用 map 就搞不定了,须要 flatMap 上场了:


public void stringToIntFlatmap() {List<String> sentences = Arrays.asList("hello world","Jia Gou Wu Dao");
    // 应用流操作
    List<String> results = sentences.stream()
            .flatMap(sentence -> Arrays.stream(sentence.split(" ")))
            .collect(Collectors.toList());
    System.out.println(results);
}

执行后果如下,能够看到后果列表中元素个数是比原始列表元素个数要多的:


[hello, world, Jia, Gou, Wu, Dao]

这里须要补充一句,flatMap操作的时候其实是先每个元素解决并返回一个新的 Stream,而后将多个 Stream 开展合并为了一个残缺的新的 Stream,如下:

peek 和 foreach 办法

peekforeach,都能够用于对元素进行遍历而后一一的进行解决。

但依据后面的介绍,peek 属于两头办法,而foreach 属于终止办法。这也就意味着 peek 只能作为管道中途的一个解决步骤,而没法间接执行失去后果,其前面必须还要有其它终止操作的时候才会被执行;而 foreach 作为无返回值的终止办法,则能够间接执行相干操作。


public void testPeekAndforeach() {List<String> sentences = Arrays.asList("hello world","Jia Gou Wu Dao");
    // 演示点 1:仅 peek 操作,最终不会执行
    System.out.println("----before peek----");
    sentences.stream().peek(sentence -> System.out.println(sentence));
    System.out.println("----after peek----");
    // 演示点 2:仅 foreach 操作,最终会执行
    System.out.println("----before foreach----");
    sentences.stream().forEach(sentence -> System.out.println(sentence));
    System.out.println("----after foreach----");
    // 演示点 3:peek 操作前面减少终止操作,peek 会执行
    System.out.println("----before peek and count----");
    sentences.stream().peek(sentence -> System.out.println(sentence)).count();
    System.out.println("----after peek and count----");
}

输入后果能够看出,peek 单独调用时并没有被执行、但 peek 前面加上终止操作之后便能够被执行,而 foreach 能够间接被执行:


----before peek----
----after peek----
----before foreach----
hello world
Jia Gou Wu Dao
----after foreach----
----before peek and count----
hello world
Jia Gou Wu Dao
----after peek and count----

filter、sorted、distinct、limit

这几个都是罕用的 Stream 的两头操作方法,具体的办法的含意在下面的表格外面有阐明。具体应用的时候,能够依据须要抉择一个或者多个进行组合应用,或者同时应用多个雷同办法的组合


public void testGetTargetUsers() {List<String> ids = Arrays.asList("205","10","308","49","627","193","111", "193");
    // 应用流操作
    List<Dept> results = ids.stream()
            .filter(s -> s.length() > 2)
            .distinct()
            .map(Integer::valueOf)
            .sorted(Comparator.comparingInt(o -> o))
            .limit(3)
            .map(id -> new Dept(id))
            .collect(Collectors.toList());
    System.out.println(results);
}

下面的代码片段的解决逻辑很清晰:

  1. 应用 filter 过滤掉不符合条件的数据
  2. 通过 distinct 对存量元素进行去重操作
  3. 通过 map 操作将字符串转成整数类型
  4. 借助 sorted 指定依照数字大小正序排列
  5. 应用 limit 截取排在前 3 位的元素
  6. 又一次应用 map 将 id 转为 Dept 对象类型
  7. 应用 collect 终止操作将最终解决后的数据收集到 list 中

输入后果:

[Dept{id=111},  Dept{id=193},  Dept{id=205}]

简略后果终止办法

依照后面介绍的,终止办法外面像 countmaxminfindAnyfindFirstanyMatchallMatchnoneMatch等办法,均属于这里说的简略后果终止办法。所谓简略,指的是其后果模式是数字、布尔值或者 Optional 对象值等。


public void testSimpleStopOptions() {List<String> ids = Arrays.asList("205", "10", "308", "49", "627", "193", "111", "193");
    // 统计 stream 操作后残余的元素个数
    System.out.println(ids.stream().filter(s -> s.length() > 2).count());
    // 判断是否有元素值等于 205
    System.out.println(ids.stream().filter(s -> s.length() > 2).anyMatch("205"::equals));
    // findFirst 操作
    ids.stream().filter(s -> s.length() > 2)
            .findFirst()
            .ifPresent(s -> System.out.println("findFirst:" + s));
}

执行后后果为:


6
true
findFirst:205

避坑揭示

这里须要补充揭示下,一旦一个 Stream 被执行了终止操作之后,后续便不能够再读这个流执行其余的操作 了,否则会报错,看上面示例:


public void testHandleStreamAfterClosed() {List<String> ids = Arrays.asList("205", "10", "308", "49", "627", "193", "111", "193");
    Stream<String> stream = ids.stream().filter(s -> s.length() > 2);
    // 统计 stream 操作后残余的元素个数
    System.out.println(stream.count());
    System.out.println("----- 上面会报错 -----");
    // 判断是否有元素值等于 205
    try {System.out.println(stream.anyMatch("205"::equals));
    } catch (Exception e) {e.printStackTrace();
    }
    System.out.println("----- 下面会报错 -----");
}

执行的时候,后果如下:


6
----- 上面会报错 -----
java.lang.IllegalStateException: stream has already been operated upon or closed
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
    at java.util.stream.ReferencePipeline.anyMatch(ReferencePipeline.java:449)
    at com.veezean.skills.stream.StreamService.testHandleStreamAfterClosed(StreamService.java:153)
    at com.veezean.skills.stream.StreamService.main(StreamService.java:176)
----- 下面会报错 -----

因为 stream 曾经被执行 count()终止办法了,所以对 stream 再执行 anyMatch办法的时候,就会报错 stream has already been operated upon or closed,这一点在应用的时候须要特地留神。

后果收集终止办法

因为 Stream 次要用于对汇合数据的解决场景,所以除了下面几种获取简略后果的终止办法之外,更多的场景是获取一个汇合类的后果对象,比方 List、Set 或者 HashMap 等。

这里就须要 collect办法出场了,它能够反对生成如下类型的后果数据:

  • 一个 汇合类,比方 List、Set 或者 HashMap 等
  • StringBuilder 对象,反对将多个 字符串进行拼接 解决并输入拼接后后果
  • 一个能够记录个数或者计算总和的对象(数据批量运算统计

生成汇合

应该算是 collect 最常被应用到的一个场景了:


public void testCollectStopOptions() {List<Dept> ids = Arrays.asList(new Dept(17), new Dept(22), new Dept(23));
    // collect 成 list
    List<Dept> collectList = ids.stream().filter(dept -> dept.getId() > 20)
            .collect(Collectors.toList());
    System.out.println("collectList:" + collectList);
    // collect 成 Set
    Set<Dept> collectSet = ids.stream().filter(dept -> dept.getId() > 20)
            .collect(Collectors.toSet());
    System.out.println("collectSet:" + collectSet);
    // collect 成 HashMap,key 为 id,value 为 Dept 对象
    Map<Integer, Dept> collectMap = ids.stream().filter(dept -> dept.getId() > 20)
            .collect(Collectors.toMap(Dept::getId, dept -> dept));
    System.out.println("collectMap:" + collectMap);
}

后果如下:


collectList:[Dept{id=22}, Dept{id=23}]
collectSet:[Dept{id=23}, Dept{id=22}]
collectMap:{22=Dept{id=22}, 23=Dept{id=23}}

生成拼接字符串

将一个 List 或者数组中的值拼接到一个字符串里并以逗号分隔开,这个场景置信大家都不生疏吧?

如果通过 for循环和 StringBuilder去循环拼接,还得思考下最初一个逗号如何解决的问题,很繁琐:


public void testForJoinStrings() {List<String> ids = Arrays.asList("205", "10", "308", "49", "627", "193", "111", "193");
    StringBuilder builder = new StringBuilder();
    for (String id : ids) {builder.append(id).append(',');
    }
    // 去掉开端多拼接的逗号
    builder.deleteCharAt(builder.length() - 1);
    System.out.println("拼接后:" + builder.toString());
}

然而当初有了 Stream,应用 collect能够轻而易举的实现:


public void testCollectJoinStrings() {List<String> ids = Arrays.asList("205", "10", "308", "49", "627", "193", "111", "193");
    String joinResult = ids.stream().collect(Collectors.joining(","));
    System.out.println("拼接后:" + joinResult);
}

两种形式都能够失去完全相同的后果,但 Stream 的形式更优雅:

拼接后:205,10,308,49,627,193,111,193

数据批量数学运算

还有一种场景,理论应用的时候可能会比拟少,就是应用 collect 生成数字数据的总和信息,也能够理解下实现形式:


public void testNumberCalculate() {List<Integer> ids = Arrays.asList(10, 20, 30, 40, 50);
    // 计算平均值
    Double average = ids.stream().collect(Collectors.averagingInt(value -> value));
    System.out.println("平均值:" + average);
    // 数据统计信息
    IntSummaryStatistics summary = ids.stream().collect(Collectors.summarizingInt(value -> value));
    System.out.println("数据统计信息:" + summary);
}

下面的例子中,应用 collect 办法来对 list 中元素值进行数学运算,后果如下:


平均值:30.0
总和:IntSummaryStatistics{count=5, sum=150, min=10, average=30.000000, max=50}

并行 Stream

机制阐明

应用并行流,能够无效利用计算机的多 CPU 硬件,晋升逻辑的执行速度。并行流通过将一整个 stream 划分为 多个片段,而后对各个分片流并行执行解决逻辑,最初将各个分片流的执行后果汇总为一个整体流。

束缚与限度

并行流相似于多线程在并行处理,所以与多线程场景相干的一些问题同样会存在,比方死锁等问题,所以在并行流终止执行的函数逻辑,必须要保障 线程平安

答复最后的问题

到这里,对于 JAVA Stream 的相干概念与用法介绍,根本就讲完了。咱们再把焦点切回本文刚开始时提及的一个问题:

Stream 相较于传统的 foreach 的形式解决 stream,到底有啥劣势

依据后面的介绍,咱们应该能够得出如下几点答案:

  • 代码更简洁、偏申明式的编码格调,更容易体现出代码的逻辑用意
  • 逻辑间解耦,一个 stream 两头解决逻辑,无需关注上游与上游的内容,只须要按约定实现本身逻辑即可
  • 并行流场景 效率 会比迭代器一一循环更高
  • 函数式接口,提早执行 的个性,两头管道操作不论有多少步骤都不会立刻执行,只有遇到终止操作的时候才会开始执行,能够防止一些两头不必要的操作耗费

当然了,Stream 也不全是长处,在有些方面也有其弊病:

  • 代码调测 debug 不便
  • 程序员从历史写法切换到 Stream 时,须要肯定的适应工夫

总结

好啦,对于 JAVA Stream 的了解要点与应用技能的论述就先到这里啦。那通过下面的介绍,各位小伙伴们是否曾经蠢蠢欲动了呢?快去我的项目中应用体验下吧!当然啦,如果有疑难,也欢送找我一起探讨探讨咯。

此外

  • 对于 Stream 中 collect 的分组、分片等进阶操作,以及对并行流的深入探讨,因为波及内容比拟多且绝对独立, 我会在后续的文档中开展专门介绍下,如果有趣味的话,能够点个关注、防止迷路。
  • 对于本文中波及的 演示代码 的残缺示例,我曾经整顿并提交到 github 中,如果您有须要,能够自取:https://github.com/veezean/JavaBasicSkills

我是悟道,聊技术、又不仅仅聊技术~

如果感觉有用,请点个关注,也能够关注下我的公众号【架构悟道】,获取更及时的更新。

期待与你一起探讨,一起成长为更好的本人。

正文完
 0