Java 8 MOOC - 第 2 节总结

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

正如我上周提到的, Sevilla Java 用户组 正在努力完成有关 lambda 和流的 Java 8 MOOC 。我们正在举办 三个会议 ,以便在参加该课程的人之间分享知识。

第二周的课程是关于 流的 ——如何使用新的流 API 来转换数据。还有一个关于 Optional 的完整部分,最初看起来相当多,但事实证明 Optional 可以做的比我原先想象的要多。

在见面会上,我们谈到了:
Optional :我认为我们很乐意使用 Optional 来防止 NullPointerException 。我们不太清楚的是 filter() map() 的示例——如果您从流中获取 Optional 值,为什么不先在流上执行映射和过滤器?例如,为什么这样做:


 list.stream()
    .findFirst()
    .map(String::trim)
    .filter(s -> s.length() > 0)
    .ifPresent(System.out::println);

什么时候可以在流中映射和过滤以获得第一个非空值?就流而言,这看起来确实是一个有趣的问题。

当其他 API 完全支持 Java 8 并返回 Optional 值时,我可以看到 Optional 更有用,然后您可以对返回值执行其他操作。

那个终端操作实际上不是终端?? :我们在会话中的示例中遇到过几次,一个示例是上面的代码(让我们将其复制到这里以便我们可以更仔细地查看它):


 list.stream()
    .findFirst()
    .map(String::trim)
    .filter(s -> s.length() > 0)
    .ifPresent(System.out::println);

findFirst() 不是 终端操作 吗?你怎么能继续做更多的操作呢?

答案当然是终端操作的返回类型也可以导致进一步的操作。上面其实是:


 list.stream()
    .findFirst()
    .map(String::trim)
    .filter(s -> s.length() > 0)
    .ifPresent(System.out::println);

我们的终端操作返回一个可选的,它允许你做进一步的操作。这种混淆的另一个例子:


 list.stream()
    .findFirst()
    .map(String::trim)
    .filter(s -> s.length() > 0)
    .ifPresent(System.out::println);

在这里, collect() 是一个终端操作,但它返回一个列表,它也允许 forEach()


 list.stream()
    .findFirst()
    .map(String::trim)
    .filter(s -> s.length() > 0)
    .ifPresent(System.out::println);

所以请注意,仅仅因为它被称为终端操作,并不意味着您不能对返回值执行其他操作。

Parallel/sequential/parallel :上周有人问为什么你可以这样写代码:


 list.stream()
    .findFirst()
    .map(String::trim)
    .filter(s -> s.length() > 0)
    .ifPresent(System.out::println);

以及这是否会让您决定流的哪些部分是并行的,哪些部分是串行处理的。第二课直截了当,宣布“最后的运算符获胜”——这意味着上述 所有 代码都将作为并行流运行。我找不到这方面的任何文档,如果我找到它,我会编辑这篇文章。

无序 :“为什么你会希望你的流是无序的?” - 答案是 unordered() 不会将你的排序集合变成一个没有顺序的集合 ,它只是说当这段代码被执行时,元素的顺序并不重要。这可能会使并行流的处理速度更快,但作为一个团队,我们认为它在顺序流上可能毫无意义。

效率优化和流操作顺序 :我们就在流中执行操作的顺序进行了 长时间的 讨论。 MOOC(事实上,大多数关于 Streams 的文档)告诉我们 a) 流是 惰性的 ,直到遇到终端运算符才会评估 b) 这可以优化流中的操作。这引发了对以下代码的讨论:


 list.stream()
    .findFirst()
    .map(String::trim)
    .filter(s -> s.length() > 0)
    .ifPresent(System.out::println);

过滤操作应该导致流中要处理的项目更少。鉴于 map() 操作不会更改 filter() 所依赖的任何内容,这段代码是否会在幕后以某种方式进行优化,以便首先实际执行过滤器?还是优化仍然会尊重流上的操作顺序?

我们的案例实际上是一个非常特殊的案例,因为 a) map() 返回与传入的参数相同的类型(即它不将 String 映射到 int )和 b) map() 不改变 filter() 正在查看的特征(即长度)。但一般来说,您不能指望这些条件是真实的——事实上我敢打赌在很多情况下它们 都不是 真实的。所以流水线操作是 按照它们被写入的顺序执行的 ,这意味着我们的 map filter 不会被重新排序为更有效的顺序。

一个好的经验法则似乎是尽可能早地在流中进行过滤——这样您就可以潜在地减少在流的每个步骤中处理的项目数量。因此我们的代码可能会更好:


 list.stream()
    .findFirst()
    .map(String::trim)
    .filter(s -> s.length() > 0)
    .ifPresent(System.out::println);


平面地图 :什么……? flatMap() 是那些一旦你掌握了它就完全有意义的方法之一,你不明白为什么它如此令人困惑。但是你第一次遇到它时,会感到困惑—— flatMap() map() 有何不同?

好吧, flatMap 用于将(例如)流压缩成一个简单的流。这就像将二维数组转换为一维数组,这样您就可以遍历所有项目而无需嵌套 for 循环。 StackOverflow 上有一个示例 ,还有更多示例可以回答 这个问题

比较器 :我们可能在某个时候都编写过比较器,这可能是我们“在过去”确实使用匿名内部类并期待用 lambda 替换它们的例子之一。


 list.stream()
    .findFirst()
    .map(String::trim)
    .filter(s -> s.length() > 0)
    .ifPresent(System.out::println);

遗憾的是,使用 lambda 仍然不能回答“我是从 o2 中减去 o1,还是从 o1 中减去 o2?”这个问题:


 list.stream()
    .findFirst()
    .map(String::trim)
    .filter(s -> s.length() > 0)
    .ifPresent(System.out::println);

但是 Java 8 中还有另一种可以拯救我们的新方法,一种几乎没有得到应有的广泛宣传的方法。有一个 Comparator.comparing() ,您可以使用它来真正轻松地定义要比较的内容。 JavaDoc 和签名看起来有点混乱,但这是方法引用突然变得有意义的地方之一:


 list.stream()
    .findFirst()
    .map(String::trim)
    .filter(s -> s.length() > 0)
    .ifPresent(System.out::println);

(这里我们实际上使用了 comparingInt 方法,因为我们要比较原始值)。就个人而言,这是我最喜欢的 Java 8 新特性之一。


下周加入我们,参加 关于 Java 8 的最后一场会议 - Lambdas 和 Streams