Java 8 中新增的特性旨在帮助程序员写出更好的代码,其中对核心类库的改进是很关键的一部分,也是本章的主要内容。对核心类库的改进主要包括集合类的 API 和新引入的流(Stream)。流使程序员得以站在更高的抽象层次上对集合进行操作。

从外部迭代到内部迭代

例:使用 for 循环计算来自伦敦的艺术家人数

int count = 0;
for (Artist artist : allArtists) {
    if (artist.isFrom("London")) {
        count++;
    }
}

一个集合进行迭代可分为外部迭代内部迭代

  • 外部迭代就是用 for 循环,for 循环其实是一个封装了迭代的语法糖,首先调用 iterator 方法,产生一个新的 Iterator 对象,进而控制整 个迭代过程,这就是外部迭代。迭代过程通过显式调用 Iterator 对象的 hasNextnext 方法完成迭代。
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());
}

传给 mapLambda 表达式只接受一个 String 类型的参数,返回一个新的 String。参数和返回值不必属于同一种类型,但是 Lambda 表达式必须是 Function 接口的一个实例(Function 接口是只包含一个参数的普通函数接口)。

webide-cloud-studio

  • 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

webide-cloud-studio

  • 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中的maxmin` 操作足以解决这一问题。例:是查找专辑中最短曲目所用的代码。

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,使用它可以方便地实现一个比较器。

  • 通用模式

maxmin 方法都属于更通用的一种编程模式。要看到这种编程模式,最简单的方法是使用 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

  • 整合操作

例:找出某张专辑上所有乐队的国籍。艺术家列表里既有个人,也有乐队。首先, 可将这个问题分解为如下几个步骤:

  1. 找出专辑上的所有表演者。(Album 类有个 getMusicians 方法,该方法返回一个 Stream 对象,包含整张专辑中所有的表演者)
  2. 分辨出哪些表演者是乐队。(使用 filter 方法对表演者进行过滤,只保留乐队)
  3. 找出每个乐队的国籍。(使用 map 方法将乐队映射为其所属国家)
  4. 将找出的国籍放入一个集合。(使用 collect(Collectors.toList()) 方法将国籍放入一个列表)
Set<String> origins = album.getMusicians()
                .filter(artist -> artist.getName().startsWith("The"))
                .map(artist -> artist.getNationality())
                .collect(toSet());

这个例子将 Stream 的链式操作展现得淋漓尽致,调用 getMusiciansfiltermap 方法都返回 Stream 对象,因此都属于惰性求值,而 collect 方法属于及早求值。map 方法接受一个 Lambda 表达式,使用该 Lambda 表达式对 Stream 上的每个元素做映射,形成一个新的 Stream

2019年3月29日

results matching ""

    No results matching ""