扯淡
在准备学 Java8 之前,我以为不会很难。所以,我就决定一边学 Java8,一边写博客。当我准备写这篇博客的时候,我发现两件都不容易。如果不是我亲身体验,我也没法知道一篇博客的背后,作者得付出多少时间和精力。这会让我在读每一篇博客时,保持对作者的敬畏之心。
本篇博客会简单介绍 Stream,以及认识是用 Stream 的 API。当我不知道这篇博客该怎么写的时候,我想试着从 Stream 的源码去解读。然后,我就傻逼似的,屁颠屁颠地去看 Stream 源码,结果可想而知,我抱着头顶上的一堆星星去床上思考人生了。Stream 的功能非常强大,给我们这些普通的程序员带来了极大的方便。这必然意味着,Java 的布道师们会在底层实现一堆逻辑非常复杂的代码。所以,学习一门新技术的时候,我们应该先知道这门技术是什么,可以干什么,然后熟练掌握,最后深究它背后的实现原理。
认识 Stream
第一眼看见 Stream 这个词的时候,会误认为和 Java 的 I/O 流有着不可描述的关系。其实,它们俩就像潘长江和曾志伟并不是兄弟一样,毫无关系。
其实和 Stream 有关系的是 Java 中的容器(我们经常使用的 List 和 Set 等集合)。在 Java8 之前,我们使用的是增强 for 语法糖和容器自身的 Iterator 来遍历数据。我们先来看下清单 1 中的代码,使用增强 for 和 Iterator 来遍历集合。
清单 1
List<String> strList =
Arrays.asList("a", "b", "c", "d");
//使用增强 for 遍历集合
for (String str : strList) {
System.out.println(str);
}
//使用 Iterator 遍历集合
Iterator iterator = strList.iterator();
while (iterator.hasNext()) {
String str = iterator.next();
System.out.println(str);
}
清单 1 中的代码使用了增强 for 语法糖和 Iterator 来遍历一个集合。其实使用增强 for 和 Iterator 是一回事。因为增强 for 是 Java 为简化我们使用 Iterator 而提供的一个语法糖,它背后的实现依然是 Iterator。大家可能会发现,无论我们是使用增强 for 还是 Iteraotr 遍历集合,都需要将集合中的元素一个个的取出来。这种将元素从集合中取出来的迭代方式叫外部迭代。
现在,我们来看下清单 2 中的代码,使用 Java8 中的 Stream 来遍历集合。
清单 2
List<String> strList =
Arrays.asList("a", "b", "c", "d");
strList.stream()
.forEach(str -> System.out.println(str));
当大家读完清单 2 中的代码后,是不是似乎有点知道 Stream 是干啥的了。通过请单 2 中的代码,我们只能看出 Stream 有遍历的集合功能,而且写法更加简单,代码的可读性也增强了。使用 Stream 遍历集合的方式叫内部迭代。
Java8 的布道师们利用了面向对象的思想,对容器的存储数据和遍历数据进行了解耦。从 Java8 开始,在绝大多数情况下,我们都会使用 Stream 来遍历集合,容器只用来存储数据,无需关心遍历。
按我的理解,Stream 就是一种新的迭代方式,它以一种更加简单的方式对数据进行处理,让代码简洁易懂。在接下来的内容中,大家会发现使用 Stream 处理数据的代码,我们能够清晰读出代码的意图,而且代码中没有多余的临时变量和命令式代码。同时,使用 Stream 操作数据,并不会改变数据源(可以是容器,可以是数组,也可以是其他类型的数据源)。在多核时代,我们可以使用 Stream 写出高效以及线程安全的代码。但这不意味着我们需要写线程相关的代码,因为 Stream 在底层已经帮我们实现了。
在接下来使用 Stream 的过程中,我们只需要在 Stream 的每个函数中传入一个函数,传入的函数只是用来告诉 Stream 需要做什么,具体该如何做,并不需要我们关心。
使用 Stream API
准备三个类
在使用 Stream API 之前,我们先准备三个类 Artist (创作音乐的个人或团队),Track (专辑中的一些曲目),Album (专辑,由若干曲目组成)。接下来的内容,都会围绕这三个类展开。这三个类的具体定义如清单 3, 4, 5 所示。
清单 3
public class Artist {
//艺术家的名字(例如 “纵贯线乐队”)
private String name;
//乐队成员(例如 “周华健"),该字段可为空
private String members;
//乐队来自哪里(例如 “台湾”)
private String origin;
//省略 set 和 get 方法
}
清单 4
public class Track {
//曲目名称
private String name;
//省略 set 和 get 方法
}
清单 5
public class Album {
//专辑名
private String name;
//专辑上所有曲目的列表
private List<Track> tracks;
//参与创作本专辑的艺术家列表
private List<Artist> musicians;
//省略 set 和 get 方法
}
案例描述
问题是,找出某张专辑上所有乐队的国籍。艺术家列表里既有个人,也有乐队,其中乐队名以 The 开头。
该问题可分解为如下几个步骤:
- 找出专辑上的所有表演者
- 分辨出哪些表演者是乐队
- 找出每个乐队的国籍
- 将找出的国籍放入一个集合
使用命令式代码实现
我们先是使用命令式风格的代码实现上述案例,代码如清单 6 所示。
清单 6
//将专辑中的艺术家为乐队的单独放入一个集合 bankList
List<Artist> artistList = album.getMusicians();
List<Artist> bankList = new ArrayList<>();
for (Artist artist : artistList) {
if (artist.getName().startsWith("The")) {
bankList.add(artist);
}
}
//找出bankList中每个乐队的国籍,并将国籍放入originList
List<String> originList = new ArrayList<>();
for (Artist artist : bankList) {
originList.add(artist.getOrigin());
}
清单 6 的代码存在如下问题:
- 存在多余的临时变量
- 样板式代码掩盖了关键代码,代码的可读性很低
使用 Stream API 实现
我们现在使用 Stream API 来重写清单 6 中的代码,如清单 7 所示。
清单 7
Set<String> originList =
album.getMusicians()
.stream()
.filter(artist -> artist.getName().startsWith("The"))
.map(artist -> artist.getOrigin())
.collect(toSet());
当大家看到清单 7 的代码时候,有没有很惊喜?反正我是为之跳跃。我先来解读下这段代码。其实,当大家熟悉 Stream 的 API 后,一眼就能看出这段代码的意图,它简直就是将问题的每一个小步骤描述了一遍,没有一点拖泥带水。首先通过 getMusicians 获取 album (专辑) 中的艺术家列表;然后使用艺术家列表构建一个 Stream,这里要说的是,所有 Collection 的子类都可以使用 stream 方法来构建一个 Stream,因为 Java8 允许接口中有 default 方法;接着调用 Stream 的 filter 方法,并告诉它筛选出 Stream 中 艺术家名字以 The 开头的数据,将筛选出的数据组织成一个新的 Stream 返回;紧接着,调用 Stream 的 map 方法,将 Stream 中的艺术家映射为艺术家的国籍,返回新的 Stream;最后,使用 Stream 的 collect 方法生成一个 Set 集合。
经过清单6 中的代码与清单 7 中的代码进行比较,我想大家会一致认同,Stream 简直太好用了,写出来的代码简洁易懂。
结束语
本篇博客只是简单介绍了 Stream 和 使用 Stream 解决问题的具体案例,在后续博客中,我会更加细致地介绍 Stream。
彩蛋
我今天要给大家介绍的是,我的学长勇哥,这个是他在开源中国的地址 https://my.oschina.net/silence88。勇哥是个完美的 Java 程序员,人帅,会做菜,弹的一手好吉他(他就是因为吉他与我嫂子相识的),会打篮球,最牛逼的还是写代码厉害。勇哥的博客值得一读,你们会看到一个屌丝程序员是如何打怪升级的。勇哥是在大四的第一个学期开始自学 Java 的,他现在顺丰的丰巢就职。
感谢大家的阅读~