63 阅读6分钟

流使程序员得以站在更高的抽象层次上对集合进行操作。

从外部迭代到内部迭代

外部迭代

for循环属于是外部迭代,其工作原理:首先调用iterator方法,产生一个新的Iterator对象,进而控制整个迭代过程,这就是外部迭代。迭代过程通过显式调用Iterator对象的hasNext和next方法完成迭代
下面两段代码是等效的,for的底层原理就是对迭代器进行封装。

int count=0;
Iterator<Artist> iterator=allArtists.iterator();
while(iterator.hasNext()) {
    Artist artist=iterator.next();
    if (artist.isFrom("London")) {
      count++;
    }
}
int count=0;
for (Artist artist : allArtists) {
    if (artist.isFrom("London")) {
      count++;
    }
}

内部迭代

在Java中,可以通过调用stream()方法,返回内部迭代中的相应接口——Stream。

long count=allArtists.stream()
.filter(artist-> artist.isFrom("London"))
.count();

上面计算来自伦敦的艺术家人数时,每种操作都对应Stream接口的一个方法。
为了找出来自伦敦的艺术家,需要对Stream对象进行过滤:filter,过滤在这里是指“只保留通过某项测试的对象”。测试由一个函数完成,根据艺术家是否来自伦敦,该函数返回true或者false。
count()方法计算给定Stream里包含多少个对象。

求值方法

惰性求值方法:返回值是Stream,只描述Stream,最终不产生新集合的方法,例如filtermap
及早求值方法:返回值是另一个值或为空,最终会从Stream产生值的方法,例如countcollect

// 不会输出任何信息
allArtists.stream()
.filter(artist-> {
    System.out.println(artist.getName());
    return artist.isFrom("London");
});
// 输出艺术家姓名
long count=allArtists.stream()
.filter(artist-> {
    System.out.println(artist.getName());
    return artist.isFrom("London");
})
.count();

判断一个操作是惰性求值还是及早求值很简单:只需看它的返回值。如果返回值是Stream,那么是惰性求值;如果返回值是另一个值或为空,那么就是及早求值。

在进行流操作时,惰性求值和及早求值常常组合在一起使用,使用这些操作的理想方式就是形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果。

在涉及集合操作(如过滤,转换等,使用惰性求值方法)的场景中,通过惰性求值,我们能把这些操作串联起来,形成一条操作链,但只执行一次迭代(计算),即在真正需要结果的地方。这样能大大提高计算效率。

常用的流操作

of

Stream的of方法使用一组初始值生成新的Stream

List<String> collected=Stream.of("a", "b", "c");

collect(toList())

collect(toList())方法由Stream里的值生成一个列表,是一个及早求值操作。

List<String> collected=Stream.of("a", "b", "c")
                          .collect(Collectors.toList());
// 断言成功,collected=Arrays.asList("a", "b", "c")
assertEquals(Arrays.asList("a", "b", "c"), collected);

这段程序展示了如何使用collect(toList())方法从Stream中生成一个列表。
很多Stream操作都是惰性求值,因此调用Stream上一系列方法之后,还需要最后再调用一个类似collect的及早求值方法。

map

返回一个新流,新流由给定函数(入参mapperFunction类型的函数式接口)对旧流的元素处理后的结果组成;

这是一个惰性求值操作;

参数:mapper,是一个Function类型的函数式接口(即Lambda表达式),会对旧流中的每一个元素进行处理;

返回值:一个新流。

// 对旧流中的每一个元素进行大写转换
List<String> collected=Stream.of("a", "b", "hello")
    .map(string-> string.toUpperCase())  
    .collect(toList());
// 断言成功,collected=asList("A", "B", "HELLO")
assertEquals(asList("A", "B", "HELLO"), collected);

传给map的Lambda表达式必须是Function接口的一个实例。下面是Function函数接口的实现。

......
@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
    ......
}

filter

返回一个新流,新流由使给定函数(入参predicatePredicate类型的函数式接口)返回true的旧流元素组成;

这是一个惰性求值操作;

参数:predicate ,是一个函数式接口(即Lambda表达式),应用于旧流中的每一个元素,以确定是否应包含该元素;

返回值:一个新流。

// 如果字符串首字母为数字,则返回true,该字符串会被保留下来。
List<String> beginningWithNumbers
=Stream.of("a", "1abc", "abc1")
        .filter(value-> isDigit(value.charAt(0)))
        .collect(toList());
// 断言成功,beginningWithNumbers=asList("1abc")
assertEquals(asList("1abc"), beginningWithNumbers);

filtermap很像,filter接受一个函数作为参数,该函数用Lambda表达式表示。
如果输入的元素使Lambda表达式返回值为true,那么该元素会被保留下来。
若要重构遗留代码,for循环中的if条件语句就是一个很强的信号,可用filter方法替代。

下面是Predicate函数接口的实现。

......
@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
    ......
}

flatMap

flatMap方法不仅可以用于替换Stream中的值(map的功能),还能将多个Stream连接成一个Stream。

接下来我们展现下flatMap将多个流连接成一个流的功能。

List<Integer> together=Stream.of(asList(1, 2), asList(3, 4))
                          .flatMap(numbers-> numbers.stream())
                          .collect(toList());
// 断言成功,together=asList(1, 2, 3, 4)
assertEquals(asList(1, 2, 3, 4), together);

max和min

查找Stream中的最大或最小元素,首先要考虑的是用什么作为排序的指标。

以查找专辑中的最短曲目为例,排序的指标就是曲目的长度。为了让Stream对象按照曲目长度进行排序,需要传给它一个Comparator对象。

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();
// 断言成功,shortestTrack=tracks.get(1)
assertEquals(tracks.get(1), shortestTrack);

reduce

reduce操作可以实现从一组值中生成一个值。count、min和max这些方法都是reduce操作。

reduce模式:reduce= initialValue+reducer
下面代码展示了使用reduce进行求和的过程,其中initialValue=0reducer (acc, element)-> acc+element),其是一个Lambda表达式,
它执行求和操作,Lambda表达式入参:当前元素elementacc,Lambda表达式返回值类型是:BinaryOperator(上一讲的【如何理解下面的问题#2】对该函数式接口做过介绍),
Lambda表达式将两个参数相加,acc是累加器,保存着当前的累加结果。

int count=Stream.of(1, 2, 3)
.reduce(0, (acc, element)-> acc+element);
// 断言成功,count=6
assertEquals(6, count);

下面代码是展开reduce操作的过程,accumulator是一个BinaryOperator类型的累加器,入参:累加器acc和当前元素element

BinaryOperator<Integer> accumulator=(acc, element)-> acc+element;
int count=accumulator.apply(
  accumulator.apply(
      accumulator.apply(0, 1),
  2),
3);

如何理解下面的问题

Stream API并不会改变集合的内容,而是描述出Stream里的内容

在使用Java 8的Stream API进行编程时,我们并不会直接操作或修改原始数据集合,而是通过描述我们想要如何处理这些数据,然后让Stream API帮助我们实现这些处理。
来看一个简单的例子:

List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squaredNums = nums.stream()
                                .map(x -> x * x)
                                .collect(Collectors.toList());

在上述代码中,我们并没有改变原有的nums列表的内容,而是创建了一个新的squaredNums列表来存放对原有列表中每个元素平方后的结果。在处理过程中,我们只是描述了我们想对原始列表中的每个元素做什么(即将每个元素平方),然后Stream API就会帮助我们完成这个操作。
因此,Stream的处理方式被称为函数式编程风格。它的好处在于,原始数据集合保持不变,有助于并发处理,以提高程序的性能。而且,处理过程的描述方式使得代码更易读、更易维护。