JDK 1.8 新特性之Stream

一、前言

流是 Java 8 的新成员,它允许你以声明式方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。此外,流还可以透明地并行处理,你无需写任何多线程代码了!

流的简短定义:从支持数据处理操作生成的元素序列

  • 元素序列:就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元素(如ArrayList 与 LinkedList)。但流的目的在于表达计算。集合讲的是数据,流讲的是计算。
  • :流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
  • 数据处理操作:流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filtermapreducefindmatchsort等。流操作可以顺序执行,也可并行执行。

此外,流操作有两个重要的特点:

  • 流水线:很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。
  • 内部迭代:与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。

你可以往集合里加东西或者删东西,但是不管什么时候,集合中的每个元素都是放在内存里的。相比之下,流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。从另一个角度来说,流就像是一个延迟创建的集合:只有在消费者要求的时候才会计算值(用管理学的话说这就是需求驱动,甚至是实时制造)。与此相反,集合则是急切创建的(供应商驱动:先把仓库装满,再开始卖,就像那些昙花一现的圣诞新玩意儿一样)。

请注意,和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。你可以从原始数据源那里再获得一个新的流来重新遍历一遍,就像迭代器一样(这里假设它是集合之类的可重复的源,如果是I/O通道就没戏了)。

java.util.stream.Stream中的Stream接口定义了许多操作,它们可以分为两大类:

  • 中间操作:诸如filtersorted等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理 – 它们很懒。这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。
  • 终端操作:终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如ListInteger,甚至void

二、构建流

2.1 由值创建流

使用Stream的静态方法of,通过显式值创建一个流。

static <T> Stream<T> of(T... values)

示例:

Stream<String> stream = Stream.of("A","B","C");
stream.map(String::toLowerCase).forEach(System.out::print);

2.2 由数组创建流

使用Arrays的静态方法stream可以从数组创建一个流。

public static <T> Stream<T> stream(T[] array)
public static <T> Stream<T> stream(T[] array,int startInclusive,int endExclusive)

注意:真多基本数据类型,都提供了对应的原始类型流特化的方法重载,如果是基本数据类型的的数组则会转换为对应的原始类型特化流,即IntStreamDoubleStreamLongStream

int[] numbers = {2, 3, 5, 7, 11, 13};
IntStream intStre= Arrays.stream(numbers);
int sum = intStre.sum();//总和是41

2.3 由集合创建流

Collection 接口增加了默认方法streamparallelStream()方法,因此任何集合的实现都可以调用这两个方法由集合创建流。区别在于,后者创建的是一个并行流。

default Stream<E> stream()
default Stream<E> parallelStream()

2.4 由文件生成流

Java 中用于处理文件等 I/O 操作的 NIO API(非阻塞 I/O)已更新,以便利用 Stream APIjava.nio.file.Files 中的很多静态方法都会返回一个流。

例如,一个很有用的方法是Files.lines,它会返回一个由指定文件中的各行构成的字符串流。使用你迄今所学的内容,你可以用这个方法看看一个文件中有多少各不相同的词:

long uniqueWords=0;
try(Stream<String> lines=Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){//流会自动关闭
    uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))//生成单词流
            .distinct()//合并重复项
            .count();//统计各个不相同的单词的个数

}catch (IOException e) {
}

我们可以使用Files.lines得到一个流,其中的每个元素都是给定文件中的一行。然后,你可以对line调用split方法将行拆分成单词。应该注意的是,你该如何使用flatMap产生一个扁平的单词流,而不是给每一行生成一个单词流。最后,把distinctcount方法链接起来,数数流中有多少各不相同的单词。

2.5 由函数生成流:创建无限流

Stream API 提供了两个静态方法来从函数生成流: Stream.iterateStream.generate 。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterategenerate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用 limit(n) 来对这种流加以限制,以避免打印无穷多个值。

迭代

使用Stream的静态方法iterate,可以创建一个无限流。

static <T> Stream<T> iterate(T seed,UnaryOperator<T> f)

示例:

Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);

iterate方法接受一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的Lambda(UnaryOperator<t>类型)。这里,我们使用Lambda n -> n + 2,返回的是前一个元素加上2。因此, iterate方法生成了一个所有正偶数的流:流的第一个元素是初始值0,然后加上2来生成新的值2,再加上2来得到新的值4,以此类推。

这种iterate操作基本上是顺序的,因为结果取决于前一次应用。请注意,此操作将生成一个无限流 – 这个流没有结尾,因为值是按需计算的,可以永远计算下去。我们说这个流是无界的。正如我们前面所讨论的,这是流和集合之间的一个关键区别。我们使用limit方法来显式限制流的大小。这里只选择了前10个偶数。然后可以调用forEach终端操作来消费流,并分别打印每个元素。

生成

iterate方法类似, generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个Supplier<T>类型的Lambda提供新的值。

static <T> Stream<T> generate(Supplier<T> s)

我们先来看一个简单的用法:

Stream.generate(Math::random).limit(5).forEach(System.out::println);

这段代码将生成一个流,其中有五个 01 之间的随机双精度数。

你可能想知道, generate方法还有什么用途。我们使用的供应源(指向Math.random的方法引用)是无状态的:它不会在任何地方记录任何值,以备以后计算使用。但供应源不一定是无状态的。你可以创建存储状态的供应源,它可以修改状态,并在为流生成下一个值时使用。

举个例子,我们将展示如何利用generate创建 测验5.4《Java 8 实战_高清中文版》中的斐波纳契数列,这样你就可以和用iterate方法的办法比较一下。但很重要的一点是,在并行代码中使用有状态的供应源是不安全的。因此下面的代码仅仅是为了内容完整,应尽量避免使用!我们会在第7章中进一步讨论这个操作的问题和副作用,以及并行流。

我们在这个例子中会使用IntStream说明避免装箱操作的代码。 IntStreamgenerate方法会接受一个IntSupplier,而不是Supplier<t>。例如,可以这样来生成一个全是1的无限流:

IntStream ones = IntStream.generate(() -> 1);

Lambda 允许你创建函数式接口的实例,只要直接内联提供方法的实现就可以。你也可以像下面这样,通过实现IntSupplier 接口中定义的getAsInt方法显式传递一个对象(虽然这看起来是无缘无故地绕圈子,也请你耐心看):

IntStream twos = IntStream.generate(new IntSupplier() {
    public int getAsInt() {
        return 2;
    }
});

generate 方法将使用给定的供应源,并反复调用 getAsInt 方法,而这个方法总是返回 2。但这里使用的匿名类和Lambda的区别在于,匿名类可以通过字段定义状态,而状态又可以用 getAsInt 方法来修改。这是一个副作用的例子。你迄今见过的所有Lambda都是没有副作用的;它们没有改变任何状态。

回到斐波纳契数列的任务上,你现在需要做的是建立一个 IntSupplier,它要把前一项的值保存在状态中,以便 getAsInt 用它来计算下一项。此外,在下一次调用它的时候,还要更新 IntSupplier 的状态。下面的代码就是如何创建一个在调用时返回下一个斐波纳契项的 IntSupplier

IntSupplier fib = new IntSupplier() {
    private int previous = 0;
    private int current = 1;

    public int getAsInt() {
        int oldPrevious = this.previous;
        int nextValue = this.previous + this.current;
        this.previous = this.current;
        this.current = nextValue;
        return oldPrevious;
    }
};
IntStream.generate(fib).limit(10).forEach(System.out::println);

前面的代码创建了一个IntSupplier的实例。此对象有可变的状态:它在两个实例变量中记录了前一个斐波纳契项和当前的斐波纳契项。 getAsInt在调用时会改变对象的状态,由此在每次调用时产生新的值。相比之下, 使用iterate的方法则是纯粹不变的:它没有修改现有状态,但在每次迭代时会创建新的元组。你应该始终采用不变的方法,以便并行处理流,并保持结果正确。请注意,因为你处理的是一个无限流,所以必须使用limit操作来显式限制它的大小;否则,终端操作(这里是forEach)将永远计算下去。同样,你不能对无限流做排序或归约,因为所有元素都需要处理,而这永远也完不成!

2.6 数值流

我们可以使用reduce方法计算流中元素的总和。例如,你可以像下面这样计算菜单的热量:

int calories = menu.stream()
        .map(Dish::getCalories)
        .reduce(0, Integer::sum);

这段代码执行结果一点问题也没有,问题在于其有一个暗含的装箱成本。每个Integer都必须拆箱成一个原始类型,再进行求和。

Stream API为我们提供了原始类型流特化,专门支持处理数值流的方法。

原始类型流特化

Java 8 引入了三个原始类型特化流接口来解决这个问题: IntStreamDoubleStreamLongStream,分别将流中的元素特化为 intlongdouble,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的 sum,找到最大元素的 max。此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性 – 即类似intInteger之间的效率差异。

映射到数值流

将流转换为特化版本的常用方法是 mapToIntmapToDoublemapToLong。这些方法和前面说的map方法的工作方式一样,只是它们返回的是一个特化流,而不是Stream<T>

转换回对象流

同样,一旦有了数值流,你可能会想把它转换回非特化流。例如,IntStream上的操作只能产生原始整数:IntStreammap操作接受的 Lambda 必须接受int并返回int(一个IntUnaryOperator)。但是你可能想要生成另一类值,比如Dish。为此,你需要访问Stream接口中定义的那些更广义的操作。要把原始流转换成一般流(每个int都会装箱成一个Integer),可以使用boxed方法,如下所示:

IntStream intStream = menu.stream().mapToInt(Dish::getCalories);// 将 Stream 转换为数值流
Stream<Integer> stream = intStream.boxed();// 将数值流转换为 Stream

默认值OptionalInt

求和的例子很容易,因为它有一个默认值: 0。但是,如果你要计算IntStream中的最大元素,就得换个法子了,因为0是错误的结果。如何区分没有元素的流和最大值真的是0的流呢?前面我们介绍了Optional类,这是一个可以表示值存在或不存在的容器。 Optional可以用IntegerString等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类型特化版本: OptionalIntOptionalDoubleOptionalLong(java.util包)。

例如,要找到IntStream中的最大元素,可以调用max方法,它会返回一个OptionalInt

OptionalInt maxCalories = menu.stream()
        .mapToInt(Dish::getCalories)
        .max();

现在,如果没有最大值的话,你就可以显式处理OptionalInt去定义一个默认值了:

int max = maxCalories.orElse(1); // 如果没有最大值的话,显式提供一个默认最大值

数值范围

和数字打交道时,有一个常用的东西就是数值范围。比如,假设你想要生成 1 和 100 之间的所有数字。 Java 8 引入了两个可以用于 IntStreamLongStream的静态方法,帮助生成这种范围:rangerangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range是不包含结束值的,而rangeClosed则包含结束值。让我们来看一个例子:

IntStream evenNumbers = IntStream.rangeClosed(1, 100)//表示范围[1-100]
        .filter(n -> n % 2 == 0);//一个从1到100的 偶数流
        System.out.println(evenNumbers.count());

这里我们用了 rangeClosed 方法来生成 1 到 100 之间的所有数字。它会产生一个流,然后你可以链接 filter 方法,只选出偶数。到目前为止还没有进行任何计算。最后,你对生成的流调用count。因为 count 是一个终端操作,所以它会处理流,并返回结果50,这正是 1 到 100(包括两端)中所有偶数的个数。请注意,比较一下,如果改用 IntStream.range(1, 100),则结果将会是49个偶数,因为range是不包含结束值的。

三、使用流

流让你从外部迭代转向内部迭代。这样,你就不用显式地管理数据集合的迭代(外部迭代)了。这种处理数据的方式很有用,因为你让 Stream API 管理如何处理数据。这样 Stream API 就可以在背后进行多种优化。此外,使用内部迭代的话, Stream API 可以决定并行运行你的代码。这要是用外部迭代的话就办不到了,因为你只能用单一线程挨个迭代。

Stream API 支持许多操作。这些操作能让你快速完成复杂的数据查询,如筛选、切片、映射、查找、匹配和归约。接下来,我们会看看一些特殊的流:数值流、来自文件和数组等多种来源的流,最后是无限流。

3.1 筛选和切片

用谓词筛选

Stream接口支持filter方法。该操作会接受一个谓词(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。

Stream<T> filter(Predicate<? super T> predicate)

示例:

List<Dish> vegetarianMenu = menu.stream()
        .filter(Dish::isVegetarian)//方法引用,检查菜肴是否适合素食者
        .collect(toList());

用谓词筛选一个流

筛选各异的元素

流还支持一个叫作distinct的方法,它会返回一个元素各异(根据流所生成元素的hashCodeequals方法实现)的流,通俗来说就是去重。

Stream<T> distinct()

例如,以下代码会筛选出列表中所有的偶数,并确保没有重复。

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0)
.distinct()
.forEach(System.out::println);

筛选流中的各异元素

截短流

流支持limit(n)方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给limit。如果流是有序的,则最多会返回前n个元素。

Stream<T> limit(long maxSize)

比如,你可以建立一个List,选出热量超过300卡路里的头三道菜:

List<Dish> dishes = menu.stream()
        .filter(d -> d.getCalories() > 300)
        .limit(3)
        .collect(toList());

你可以看到,该方法只选出了符合谓词的头三个元素,然后就立即返回了结果。

截短流

请注意limit也可以用在无序流上,比如源是一个Set。这种情况下, limit的结果不会以任何顺序排列。

跳过元素

流还支持skip(n)方法,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流。注意: limit(n)skip(n)是互补的!

Stream<T> skip(long n)

例如,下面的代码将跳过超过300卡路里的头两道菜,并返回剩下的。

List<Dish> dishes = menu.stream()
        .filter(d -> d.getCalories() > 300)
        .skip(2)
        .collect(toList());

在流中跳过元素

3.2 映射

一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表中选择一列。Stream API 也通过 mapflatMap 方法提供了类似的工具。

对流中每一个元素应用函数

流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”不是去“修改”)。

<R> Stream<R> map(Function<? super T,? extends R> mapper)

例如,给定一个单词列表,你想要返回另一个列表,显示每个单词中有几个字母:

List<String> words = Arrays.asList("Java 8", "Lambdas", "In", "Action");
List<Integer> wordLengths = words.stream()
        .map(String::length)
        .collect(toList());

流的扁平化

你已经看到如何使用map方法返回列表中每个单词的长度了。

让我们拓展一下:对于一张单词表,如何返回一张列表,列出里面各不相同的字符呢?例如,给定单词列表["Hello","World"],你想要返回列表["H","e","l","o","W","r","d"]。你可能会认为这很容易,你可以把每个单词映射成一张字符表,然后调用distinct来过滤重复的字符。

第一个版本可能是这样的:

List<String> words = Arrays.asList("Java 8", "Lambdas", "In", "Action");
List<String[]> wordLengths = words.stream()
        .map(word -> word.split(""))
        .distinct()
        .collect(toList());

答案是错误的。这个方法的问题在于,传递给map方法的Lambda为每个单词返回了一个String[](String列 表 )。因此,map返回的流实际上是Stream<String[]>类型的。你真正想要的是用Stream<String>来表示一个字符流。

不正确地使用map找出单词列表中各不相同的字符

尝试使用map和Arrays.stream()

首先,你需要一个字符流,而不是数组流。有一个叫作Arrays.stream()的方法可以接受一个数组并产生一个流,例如:

String[] arrayOfWords = {"Goodbye", "World"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords);

把它用在前面的那个流水线里,看看会发生什么:

List<String> words = Arrays.asList("Java 8", "Lambdas", "In", "Action");
List<Stream<String>> wordLengths = words.stream()
    .map(word -> word.split(""))//将每个单词转换为由其字母构成的数组
    .map(Arrays::stream) //让每个数组变成一个单独的流
    .distinct()
    .collect(toList());

当前的解决方案仍然搞不定!这是因为,你现在得到的是一个流的列表!的确,你先是把每个单词转换成一个字母数组,然后把每个数组变成了一个独立的流。

使用flatMap

你可以像下面这样使用flatMap来解决这个问题:

List<String> uniqueCharacters = words.stream()
        .map(w -> w.split(""))//将每个单词转换为由其字母构成的数组
        .flatMap(Arrays::stream)//将各个生成流扁平化为单个流
        .distinct()
        .collect(Collectors.toList());

使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流。

下图说明了使用flatMap方法的效果:

使用flatMap找出单词列表中各不相同的字符

一言以蔽之, flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。

问题:给定两个数字列表,如何返回所有的数对呢?例如,给定列表[1, 2, 3]和列表[3, 4],应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]。为简单起见,你可以用有两个元素的数组来代表数对。

答案:你可以使用两个map来迭代这两个列表,并生成数对。但这样会返回一个Stream<Stream<Integer[]>>。你需要让生成的流扁平化,以得到一个Stream<Integer[]>(这正是flatMap所做的)。

List<Integer> numbers1=Arrays.asList(1,2,3);
List<Integer> numbers2=Arrays.asList(3,4);
List<Stream<int[]>> tempMap=numbers1.stream().map(i -> numbers2.stream().map(j -> new int[] {i,j}))
        .collect(Collectors.toList());
List<int[]> tempFlatMap=numbers1.stream().flatMap(i -> numbers2.stream().map(j -> new int[] {i,j}))
        .collect(Collectors.toList());

3.3 查找和匹配

另一个常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性。

Stream API 通过allMatchanyMatchnoneMatchfindFirstfindAny方法提供了这样的工具。

检查谓词是否至少匹配一个元素

anyMatch方法可以回答“流中是否有一个元素能匹配给定的谓词”。

boolean anyMatch(Predicate<? super T> predicate)

比如,你可以用它来看看菜单里面是否有素食可选择:

if (menu.stream().anyMatch(Dish::isVegetarian)) {
    System.out.println("The menu is (somewhat) vegetarian friendly!!");
}

anyMatch方法返回一个boolean,因此是一个终端操作。

检查谓词是否匹配所有元素

allMatch方法的工作原理和anyMatch类似,但它会看看流中的元素是否都能匹配给定的谓词。

boolean allMatch(Predicate<? super T> predicate)

比如,你可以用它来看看菜品是否有利健康(即所有菜的热量都低于1000卡路里):

boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);

allMatch相对的是noneMatch。它可以确保流中没有任何元素与给定的谓词匹配。

短路

anyMatchallMatchnoneMatch这三个操作都用到了我们所谓的短路,这就是大家熟悉的Java&&||运算符短路在流中的版本。

对于流而言,某些操作(例如allMatchanyMatchnoneMatchfindFirstfindAny)不用处理整个流就能得到结果。只要找到一个元素,就可以有结果了。同样, limit也是一个短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。在碰到无限大小的流的时候,这种操作就有用了:它们可以把无限流变成有限流。

查找元素

findAny方法将返回当前流中的任意元素。它可以与其他流操作结合使用。

Optional<T> findAny()

比如,你可能想找到一道素食菜肴。你可以结合使用filterfindAny方法来实现这个查询:

Optional<Dish> dish =
        menu.stream()
        .filter(Dish::isVegetarian)
        .findAny();

流水线将在后台进行优化使其只需走一遍,并在利用短路找到结果时立即结束。

不过慢着,代码里面的Optional是个什么玩意儿?Optional<T>类(java.util.Optional)是一个容器类,代表一个值存在或不存在。Java 8 的库设计人员引入了Optional<T>,这样就不用返回众所周知容易出问题的null了。

查找第一个元素

有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序(比如由List或排序好的数据列生成的流)。对于这种流,你可能想要找到第一个元素。为此有一个findFirst方法,它的工作方式类似于findany

Optional<T> findFirst()

例如,给定一个数字列表,下面的代码能找出第一个平方能被3整除的数:

List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree = someNumbers
        .stream().map(x -> x * x)
        .filter(x -> x % 3 == 0)
        .findFirst();

何时使用findFirstfindAny

你可能会想,为什么会同时有findFirstfindAny呢?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。

3.4 归约

到目前为止,你见到过的终端操作都是返回一个booleanallMatch之类的)、 voidforEach)或Optional对象(findAny等)。你也见过了使用collect来将流中的所有元素组合成一个List

在本节中,你将看到如何把一个流中的元素组合起来,使用reduce操作来表达更复杂的查询,此类查询需要将流中所有元素反复结合起来,得到一个值。这样的查询可以被归类为归约操作将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠fold),因为你可以将这个操作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。

元素求和

在我们研究如何使用reduce方法之前,先来看看如何使用for-each循环来对数字列表中的元素求和:

int sum = 0;
for (int x : numbers) {
    sum += x;
}

numbers中的每个元素都用加法运算符反复迭代来得到结果。通过反复使用加法,你把一个数字列表归约成了一个数字。这段代码中有两个参数:

  • 总和变量的初始值,在这里是0
  • 将列表中所有元素结合在一起的操作,在这里是+

要是还能把所有的数字相乘,而不必去复制粘贴这段代码,岂不是很好?这正是reduce操作的用武之地,它对这种重复应用的模式做了抽象。你可以像下面这样对流中所有的元素求和:

int sum = numbers.stream().reduce(0, (a, b) -> a + b);
// 方法引用
int sum = numbers.stream().reduce(0, Integer::sum);

reduce接受两个参数:

  • 一个初始值,这里是0;
  • 一个BinaryOperator<T>来将两个元素结合起来产生一个新值,这里我们用的是lambda (a, b) -> a + b

使用reduce求和

无初始值

reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象:

Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));

为什么它返回一个Optional<Integer>呢?考虑流中没有任何元素的情况。reduce操作无法返回其和,因为它没有初始值。这就是为什么结果被包裹在一个Optional对象里,以表明和可能不存在。

最大值和最小值

原来,只要用归约就可以计算最大值和最小值了!让我们来看看如何利用刚刚学到的reduce来计算流中最大或最小的元素。正如你前面看到的, reduce接受两个参数:

  • 一个初始值
  • 一个Lambda来把两个流元素结合起来并产生一个新值

你可以像下面这样使用reduce来计算流中的最大值,如下图所示。

Optional<Integer> max = numbers.stream().reduce(Integer::max);

要计算最小值,你需要把Integer.min传给reduce来替换Integer.max

Optional<Integer> min = numbers.stream().reduce(Integer::min);

你当然也可以写成Lambda (x, y) -> x < y ? x : y而不是Integer::min,不过后者比较易读。

归约方法的优势与并行化

相比于前面写的逐步迭代求和,使用reduce的好处在于,这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce操作。而迭代式求和例子要更新共享变量sum,这不是那么容易并行化的。如果你加入了同步,很可能会发现线程竞争抵消了并行本应带来的性能提升!这种计算的并行化需要另一种办法:将输入分块,分块求和,最后再合并起来。但这样的话代码看起来就完全不一样了。使用流来对所有的元素并行求和时,你的代码几乎不用修改: stream()换成了parallelStream()

int sum = numbers.parallelStream().reduce(0, Integer::sum);

但要并行执行这段代码也要付一定代价,传递给reduceLambda不能更改状态(如实例变量),而且操作必须满足结合律才可以按任意顺序执行。


流操作:无状态和有状态

你已经看到了很多的流操作。乍一看流操作简直是灵丹妙药,而且只要在从集合生成流的时候把Stream换成parallelStream就可以实现并行。

当然,对于许多应用来说确实是这样,就像前面的那些例子。你可以把一张菜单变成流,用filter选出某一类的菜肴,然后对得到的流做map来对卡路里求和,最后reduce得到菜单的总热量。这个流计算甚至可以并行进行。但这些操作的特性并不相同。它们需要操作的内部状态还是有些问题的。

诸如mapfilter等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作一般都是无状态的:它们没有内部状态(假设用户提供的Lambda或方法引用没有内部可变状态)。

但诸如reducesummax等操作需要内部状态来累积结果。在上面的情况下,内部状态很小。在我们的例子里就是一个intdouble。不管流中有多少元素要处理,内部状态都是有界的。

相反,诸如sortdistinct等操作一开始都和filtermap差不多 – 都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作

赞赏

猜你喜欢

转载自blog.csdn.net/fanxiaobin577328725/article/details/82119982