Java 8 中新增的特性旨在帮助程序员写出更好的代码,其中对核心类库的改进是很关键的一部分,也是本章的主要内容。对核心类库的改进主要包括集合类的 API 和新引入的流(Stream)。流使程序员得以站在更高的抽象层次上对集合进行操作。
从外部迭代到内部迭代
例:使用 for 循环计算来自伦敦的艺术家人数
int count = 0;
for (Artist artist : allArtists) {
if (artist.isFrom("London")) {
count++;
}
}
一个集合进行迭代可分为外部迭代和内部迭代:
- 外部迭代就是用
for
循环,for
循环其实是一个封装了迭代的语法糖,首先调用iterator
方法,产生一个新的Iterator
对象,进而控制整 个迭代过程,这就是外部迭代。迭代过程通过显式调用Iterator
对象的hasNext
和next
方法完成迭代。
int count = 0;
Iterator<Artist> iterator = allArtists.iterator();
while(iterator.hasNext()) {
Artist artist = iterator.next();
if (artist.isFrom("London")) {
count++;
}
}
- 另一种方法就是内部迭代,首先要注意
stream()
方法的调用,外部迭代中调用iterator()
的作用一样。该方法不是返回一个控制迭代的Iterator
对象,而是返回内部迭代中的相应接口:Stream
(Stream 是用函数式编程方式在集合类上进行复杂操作的工具)。
long count = allArtists.stream()
.filter(artist -> artist.isFrom("London"))
.count();
实现机制
# 只过滤,不计数
allArtists.stream()
.filter(artist -> artist.isFrom("London"));
这行代码并未做什么实际性的工作,filter
只刻画出了 Stream
,但没有产生新的集合。像 filter
这样只描述 Stream
,最终不产生新集合的方法叫作惰性求值方法;而像 count
这样最终会从 Stream
产生值的方法叫作及早求值方法;例:在过滤器中加入一条 println
语句,来输出艺术家的名字,由于使用了惰性求值将不会有任何输出,如果加入一个拥有终止操作的流,则就能正常输出。
判断一个操作是惰性求值还是及早求值:
只需看它的返回值。如果返回值是 Stream
,那么是惰性求值;如果返回值是另一个值或为空,那么就是及早求值。使用这些操作的理想方式就是形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果,整个过程和建造者模式有共通之处。建造者模式使用一系列操作设置属性和配置,最后调用一个 build
方法,这时,对象才被真正创建。
为什么要区分惰性求值和及早求值?
只有在对需要什么样的结果和操作有了更多了解之后,才能更有效率地进行计算。 例如:如果要找出大于 10 的第一个数字,那么并不需要和所有元素去做比较, 只要找出第一个匹配的元素就够了。这也意味着可以在集合类上级联多种操作,但迭代只需一次。这也是函数编程中惰性计算的特性,即只在需要产生表达式的值时进行计算。这样代码更加清晰,而且省掉了多余的操作。
常用的流操作
- collect(toList())
collect(toList())
方法由 Stream
里的值生成一个列表,是一个及早求值操作。
List<String> collected = Stream.of("a", "b", "c")
.collect(Collectors.toList());
这段程序展示了如何使用 collect(toList())
方法从 Stream
中生成一个列表,然后进行一些 Stream
上的操作,继而是 collect
操作(由于很多 Stream 操作都是惰性求值,最后还需要再调用一个类似 collect 的及早求值方法)。
- map
如果有一个函数可以将一种类型的值转换成另外一种类型,map
操作就可以使用该函数,将一个流中的值转换成一个新的流;例:编写一段代码,将一组字符串转换成对应的大写形式。
List<String> collected = Stream.of("a", "b", "hello")
.map(string -> string.toUpperCase())
.collect(toList());
}
传给 map
的 Lambda
表达式只接受一个 String
类型的参数,返回一个新的 String
。参数和返回值不必属于同一种类型,但是 Lambda
表达式必须是 Function
接口的一个实例(Function 接口是只包含一个参数的普通函数接口)。
- filter
遍历数据并检查其中的元素时,可尝试使用 Stream
中提供的新方法 filter
;例:要找出一组字符串("1abc" 和 "abc")中以数字开头的字符串。
List<String> beginningWithNumbers
= Stream.of("a", "1abc", "abc1")
.filter(value -> isDigit(value.charAt(0)))
.collect(toList());
和 map
很像,filter
接受一个函数作为参数,该函数用 Lambda
表达式表示。该 Lambda
表达式的函数接口正是前面章节中介绍过的 Predicate
。
- flatMap
flatMap 方法可用 Stream 替换值,然后将多个 Stream 连接成一个 Stream。例:假设有一个包含多个列表的流,现在希望得到所有数字的序列。
List<Integer> together = Stream.of(asList(1, 2), asList(3, 4))
.flatMap(numbers -> numbers.stream())
.collect(toList());
flatMap
方法的相关函数接口和 map
方法的一样,都是 Function
接口,只是方法的返回值限定为 Stream
类型罢了。
- max和min
Stream
上常用的操作之一是求最大值和最小值。Stream API中的
max和
min` 操作足以解决这一问题。例:是查找专辑中最短曲目所用的代码。
List<Track> tracks = asList(new Track("Bakai", 524),
new Track("Violets for Your Furs", 378),
new Track("Time Was", 451));
Track shortestTrack = tracks.stream()
.min(Comparator.comparing(track -> track.getLength()))
.get();
为了让 Stream
对象按照曲目长度进行排序,需要传给它一个 Comparator
对象。Java 8 提供了一个新的静态方法 comparing
,使用它可以方便地实现一个比较器。
- 通用模式
max
和 min
方法都属于更通用的一种编程模式。要看到这种编程模式,最简单的方法是使用 for 循环重代码。
List<Track> tracks = asList(new Track("Bakai", 524),
new Track("Violets for Your Furs", 378),
new Track("Time Was", 451));
Track shortestTrack = tracks.get(0);
for (Track track : tracks) {
if (track.getLength() < shortestTrack.getLength()) {
shortestTrack = track;
}
}
- reduce
reduce
操作可以实现从一组值中生成一个值。例:执行求和操作,有两个
参数:传入 Stream
中的当前元素和 acc
。将两个参数相加,acc
是累加器,保存着当前的累加结果。
int count = Stream.of(1, 2, 3)
.reduce(0, (acc, element) -> acc + element);
Lambda
表达式的返回值是最新的 acc
,是上一轮 acc
的值和当前元素相加的结果。reduce
的类型是 BinaryOperator
。
- 整合操作
例:找出某张专辑上所有乐队的国籍。艺术家列表里既有个人,也有乐队。首先, 可将这个问题分解为如下几个步骤:
- 找出专辑上的所有表演者。(Album 类有个 getMusicians 方法,该方法返回一个 Stream 对象,包含整张专辑中所有的表演者)
- 分辨出哪些表演者是乐队。(使用 filter 方法对表演者进行过滤,只保留乐队)
- 找出每个乐队的国籍。(使用 map 方法将乐队映射为其所属国家)
- 将找出的国籍放入一个集合。(使用 collect(Collectors.toList()) 方法将国籍放入一个列表)
Set<String> origins = album.getMusicians()
.filter(artist -> artist.getName().startsWith("The"))
.map(artist -> artist.getNationality())
.collect(toSet());
这个例子将 Stream
的链式操作展现得淋漓尽致,调用 getMusicians
、filter
和 map
方法都返回 Stream
对象,因此都属于惰性求值,而 collect
方法属于及早求值。map
方法接受一个 Lambda
表达式,使用该 Lambda
表达式对 Stream
上的每个元素做映射,形成一个新的 Stream
。
2019年3月29日