java8 -stream part1

使用流操作来表达复杂的数据处理查询。

集合是Java中使用多的API。要是没有集合,还能做什么呢?几乎每个Java应用程序都会制 造和处理集合。集合对于很多编程任务来说都是非常基本的:它们可以让你把数据分组并加以处理。例如,你希望根据创建一个银行的交易集合来分析用户的消费行为。所以,你希望通过处理整个集合来了解用户的消费的消费情况,这在银行业务方面很常见,也很重要,但是java在集合操作方面却远远算不上完美。

  • 首先,典型集合处理方式类似SQL的操作,例如“查找”(例如,查找最大的一笔消费)或“分组”(例如,将与杂货店购物相关的所有交易分组)。 大多数数据库允许你以声明方式做此类操作。 例如,以下SQL查询语句就可以选出最大一笔消费的事务ID:“SELECT id,MAX(value)from transactions”
  • 其次,要是要处理大量元素又该怎么办呢?为了提高性能,你需要并行处理,并利用多核架构。 但写并行代码比用迭代器还要复杂,而且调试起来也够受的!
    Java SE 8来了! Java API设计人员增加stream的新成员,它允许你以声明性方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现)。此外,流可以利用多核架构,你无需写任何多线程代码了! 听起来不错,不是吗? 这就是本节将要探讨的内容。
    在我们详细探讨stream可以做什么之前,我们先写两个简单的例子来对比一下。现在有如下需求,假设我们需要查找grocery类型的所有transactions ,并返回按transaction value的降序排序的transaction ID列表。 在Java SE 7中,我们一般会如下操作:
例1
//extract transactions of type matching parameter type
        List<Transaction> groceryTransactions = new ArrayList<>();
        for(Transaction t: transactions){
            if(t.getType() == Transaction.GROCERY){
                groceryTransactions.add(t);
            }
        }

        //sort by value descending
        Collections.sort(groceryTransactions, new Comparator<Transaction>(){
            public int compare(Transaction t1, Transaction t2){
                return t2.getValue().compareTo(t1.getValue());
            }
        });

        //get transaction IDs
        List<Integer> transactionIds = new ArrayList<>();
        for(Transaction t: groceryTransactions){
            transactionIds.add(t.getId());
        }

采用Java SE 8 streams的编程风格,代码如下:

例2
 List<Integer> transactionsIds = transactions.stream() //user transactions ArrayList as source
                .filter(t -> t.getType().equals(Transaction.GROCERY)) //filter on type
                .sorted(comparing(Transaction::getValue).reversed()) //sort using value, but reversed
                .map(Transaction::getId) //extract IDs
                .collect(toList()); //save in List

图1显示了Java SE 8代码。我们使用List的stream()方法从transactions集合钟获取流,接下来,将几个操作(过滤,排序,映射,收集)链接在一起以形成一个管道,该过程可以视为数据的查询。
图1

那么如何并行化查询呢? 在Java SE 8中,它很容易:只需要将stream() 替换成parallelStream() 即可,Streams API 根据你计算机的内核自动分解成多个任务并行。如下所示:

例3
 List<Integer> transactionsIds = transactions.parallelStream() //parallelizing the code
                .filter(t -> t.getType().equals(Transaction.GROCERY))
                .sorted(comparing(Transaction::getValue).reversed())
                .map(Transaction::getId)
                .collect(toList());

如果你现在看着有点稀里糊涂,没关系,你只需要熟悉lambda表达式(t -> t.getType().equals(Transaction.GROCERY)Y)和方法引用(Transaction::getId)就好了,原理将会在后面详细讲解。
现在你只需要将流看作类似于sql查询的一种抽象的高效的集合操作。 此外,可以使用lambda表达式简洁地对这些操作进行参数化。

开始流的学习

让我们先从理论开始学习吧, 流的是什么? 一个简短的定义是“Stream是一个来自支持聚合操作源的元素的序列“。流简单的来说有如下特点:

  • 元素序列:一个Stream向外提供了一个这样的接口:特定元素类型的值的序列,但是Stream并不实际持有,也就是说不存储这些元素,它们是在有需求时才会被计算。
  • :以提供Stream进行计算消费的源,这些源有Collection集合 Array数组或I/O资源等。
  • 聚合操作:Stream支持类似SQL操作和函数式编程的大部分操作,比如:filter, map, reduce, find, match, sorted。
    此外,Stream操作不同于Collection操作有两个根本的地方:
  • 管道Pipelining:许多流Stream操作返回流Stream自身,这就允许对其操作可以像链条一样排列,变成一个管道,这其中也会激活比如懒加载和short-circuiting操作。
  • 内部迭代:相比于集合Collection是显式迭代(需要我们编码完成迭代),Stream操作是在其内部完成迭代操作。
    图2 显示上面代码2中 Stream代码的内部工作流程:
    图2

图2 显示:
我们首先通过调用stream()从交易transactions List中获得Stream对象,数据源就是transactions List,为流提供了一系列可操作元素。接下来,我们对流进行了一系列的聚合操作:filter(通过predicate过滤元素),sorted(通过comparator进行排序)和map(将transactions 转化成 ids)。 除了collect之外的所有这些操作都返回一个Stream,因此它们可以被链接成一个管道,可以看成是基于源数据集合的一个查询操作。如同SQL基于数据表的有条件查询语句一样。
在调用collect之前,所有聚合操作实际上没有工作。collect操作开始正式处理pipeline并返回一个结果(返回的结果并不一定全部都是stream,比如这里就返回一个List),我们先不用理解collert的原理。我们先将collect视为一个操作,该操通过不同的参数(toSet(),toList(),toCollection),将流中的元素累计汇总到一个集合当中,这里参数是toList(),所以将Stream 转化成了一个List。

在我们深入学习stream的各种使用方式之前,我们需要先搞清楚StreamCollections之间的区别。

Streams VS Collections

Java现有的集合概念和新的流概念都提供了接口,来配合代表元素型有序值的数据接口。所 谓有序,就是说我们一般是按顺序取用值,而不是随机取用的。那这两者有什么区别呢?简而言之,集合是关于数据的,而流是关于计算的。
我们先来打个直观的比方吧。比如说存在DVD里的电影,这就是一个集合(也许是字节,也 许是帧,这个无所谓),因为它包含了整个数据结构。现在再来想想在互联网上通过视频流看同 样的电影。现在这是一个流(字节流或帧流)。流媒体视频播放器只要提前下载用户观看位置的 那几帧就可以了,这样不用等到流中大部分值计算出来,你就可以显示流的开始部分了(想想观 看直播足球赛)。特别要注意,视频播放器可能没有将整个流作为集合,保存所需要的内存缓冲 区——而且要是非得等到后一帧出现才能开始看,那等待的时间就太长了。出于实现的考虑, 你也可以让视频播放器把流的一部分缓存在集合里,但和概念上的差异不是一回事。 粗略地说,集合与流之间的差异就在于什么时候进行计算。集合是一个内存中的数据结构, 它包含数据结构中目前所有的值——集合中的每个元素都得先算出来才能添加到集合中。(你可以往集合里加东西或者删东西,但是不管什么时候,集合中的每个元素都是放在内存里的,元素都得先算出来才能成为集合的一部分。) 相比之下,流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。
图3用DVD对比在线流媒体的例子展示了流和集合之间的差异。
图3(摘自java 8 in action)
使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。 相反, Streams库使用内部迭代——它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出 一个函数说要干什么就可以了。下面的代码列表说明了这种区别。

例4
List<String> transactionIds = new ArrayList<>(); 
for(Transaction t: transactions){
    transactionIds.add(t.getId()); 
}
例5
List<Integer> transactionIds = 
    transactions.stream()
                .map(Transaction::getId)
                .collect(toList());

在例4中,我们按顺序显式迭代transactions列表以提取每个事务ID并将其添加到集合当中。 相反,当使用流时,没有明确的迭代。 例5中的代码构建了一个查询,其中参数化map操作以提取事务ID,并且collect操作将生成的Stream转换为List
内部迭代与外部迭代

至此,你应该明白流是什么了,接下来我们介绍流的具体使用方式。

流操作

java.util.stream.Stream中的Stream接口定义了许多操作。它们可以分为两大类。在例2中,我们有如下几个操作:

  • filter、map和limit可以连成一条流水线;
  • collect触发流水线执行并关闭它。
    可以连接起来的流操作称为中间操作(intermediate operations),关闭流的操作称为终端操作(terminal operations)。
    中间操作,诸如filter或sorted等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查 询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理——它们很懒。 这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。
    终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如List、Integer,甚 至void。
例6
 List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
        List<Integer> twoEvenSquares = numbers.stream()
                .filter(n -> {
                    System.out.println("filtering " + n);
                    return n % 2 == 0; //returns true if n is even
                })
                .map(n -> {
                    System.out.println("mapping " + n);
                    return n * n; //square n
                })
                .limit(2) //only return the first two squared evens
                .collect(Collectors.toList()); //save in List

输出如下:

filtering 1
filtering 2
mapping 2
filtering 3
filtering 4
mapping 4

你可能对结果很惊讶?为什么只记算2个偶数的平方数呢?这是因为**limit(2)**操作和一种称为短路的技巧(我们后面会详细介绍),我们只需要处理部分流而不是全部流来返回结果。这类似于使用and运算符链接的布尔表达式,只要一个表达式返回false,我们就可以推断出整个表达式为false,而不用全部记算完毕。例6中,limit限制返回得size为2. 其次,尽管filter和map是两个独立的操作,但它们合并到同一次遍历中了(我们把这种技术叫作循环合并)。
中 间 操 作 \color{#123456}{\normalsize\mathbf{中间操作}}

操 作类 型返回类型操作参数函数描述符
filter中间StreamPredicateT -> boolean
distinct中间Stream
skip中间Streamlong
limit中间Stream
map中间StreamFunction<T, R>T -> R
flatMap中间StreamFunction<T, Stream>T -> Stream
sorted中间StreamComparator(T, T) -> int
anyMatch终端booleanPredicateT -> boolean
noneMatch终端booleanPredicateT -> boolean
allMatch终端booleanPredicateT -> boolean
findAny终端Optional
findFirst终端Optional
forEach终端voidConsumerT -> void
collect终端RCollector<T, A, R>
reduce终端OptionalBinaryOperator(T, T) -> T
count终端long

总结一下到目前为止我们学到的知识,一般来说,流的使用一般包括三件事:

  • 一个数据源(如集合)来执行一个查询;
  • 一个中间操作链,形成一条流的流水线;
  • 一个终端操作,执行流水线,并能生成结果。
    接下来让我们学习一下stream的一些常用操作,更多内容请参阅java.util .stream.Stream.
  • 过滤
    对流中的元素进行过滤操作,有如下几个操作:
  • filter(Predicate): predicate (java.util.function.Predicate)
  • distinct: 它会返回一个元素各异(根据流所生成元素的 hashCode和equals方法实现)的流.
  • limit(n): 该方法会返回一个不超过给定长度的流。所需的长度作为参数传递 给limit。如果流是有序的,则多会返回前n个元素。
  • skip(n): * 返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一 个空流。
    查找和匹配
    另一个常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性。Stream API通过allMatch、anyMatch、noneMatch、findFirst和findAny方法提供了这样的工具。它们都将
    predicate作为参数并返回一个boolean作为结果(因此,它们是终端操作)。 比如, 你可以使用allMatch*去匹配transactions的stream中的value值大于100的元素 。如下所示:
例7
boolean expensive =
    transactions.stream()
                .allMatch(t -> t.getValue() > 100);

此外,Stream接口提供了findFirstfindAny方法,用于从流中检索任意元素。它们可以和流的其他方法结合使用,比如filterfindFirstfindAny都会返回一个Optional对象,如下所示:

Optional<Transaction> any = transactions.stream()
                .filter(t -> t.getType() == Transaction.GROCERY)
                .findAny();

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

Optional类(java.util.Optional)是一个容器类,代表一个值存在或不存在。在 上面的代码中,findAny可能什么元素都没找到。。Java 8的库设计人员引入了Optional,这 样就不用返回众所周知容易出问题的null了。我们在这里不会详细讨论Optional,以后会详细解释你的代码如何利用Optional,避免和null检查相关的bug。不过现在,了解一下Optional里面几种可以迫使你显式地检查值是否存在或处理值不存在的情形的方法也不错。

  • isPresent():将在Optional包含值的时候返回true, 否则返回false。

  • ifPresent(Consumer block):会在值存在的时候执行给定的代码块。

  • T get():会在值存在时返回值,否则抛出一个NoSuchElement异常。

    • T orElse(T other)*: 会在值存在时返回值,否则返回一个默认值。
      例如,如果存在Transaction,我们可以选择使用ifPresent方法对可选对象进行操作,如例9所示(打印事务):
transactions.stream()
              .filter(t -> t.getType() == Transaction.GROCERY)
              .findAny()
              .ifPresent(System.out::println);

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


例如,我们想要将流中的元素提取属性值,如例10中,我们返回单词列表中的长度列表:

例10
List<String> words = Arrays.asList("Oracle", "Java", "Magazine");
        List<Integer> wordLengths =
                words.stream()
                        .map(String::length)
                        .collect(toList());

归约(Reducing)
到目前为止,你见到过的终端操作都是返回一个boolean(allMatch之类的)、void (forEach)或Optional对象(findAny等)。你也见过了使用collect来将流中的所有元素组 合成一个List。
你还可以把一个流中的元素组合起来,使用reduce操作来表达更复杂的查 询,比如“计算Transaction中的总value值”或“查询Transaction中最大的ID”。此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer。这样的查询可以被归类为归约操作 (将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠(fold),因为你可以将这个操 作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。
在我们研究如何使用reduce方法之前,先来看看如何使用for-each循环来对数字列表中的 元素求和:

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

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

  • 总和变量的初始值,在这里是0;
  • 将列表中所有元素结合在一起的操作,在这里是+。
    要是能把所有的数字相加,而不必去复制粘贴这段代码,岂不是很好?这正是reduce操 作的用武之地,它对这种重复应用的模式做了抽象。你可以像下面这样对流中所有的元素求和:
例11
int sum = numbers.stream().reduce(0, (a, b) -> a + b); 

reduce接受两个参数:

  • 一个初始值,这里是0;
  • 一个BinaryOperator来将两个元素结合起来产生一个新值,这里我们用的是 lambda (a, b) -> a + b.
    你也很容易把所有的元素相乘,只需要将另一个Lambda:(a, b) -> a * b传递给reduce 操作就可以了:
例12
int product = numbers.stream().reduce(1, (a, b) -> a * b); 

下图展示了reduce操作是如何作用于一个流的:Lambda反复结合每个元素,直到流被归约 成一个值。
使用reduce来对流中的数字求和

你可以使用方法引用让这段代码更简洁。在Java 8中,Integer类现在有了一个静态的sum 方法来对两个数求和,这恰好是我们想要的,用不着反复用Lambda写同一段代码了:

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

数值流(Numeric Streams)
我们在前面看到了可以使用reduce方法计算流中元素的总和。例如,你可以像下面这样计算Transaction集合的value总和:

 double statement =
                transactions.stream()
                        .map(Transaction::getValue)
                        .reduce(0.0, Double::sum);

这段代码的问题是,它有一个暗含的装箱成本。每个Integer都必须拆箱成一个原始类型, 再进行求和。要是可以直接像下面这样调用sum方法,岂不是更好?

例13
double statement =
                transactions.stream()
                        .map(Transaction::getValue)
                       .sum(); // error since Stream has no sum method

但这是不可能的。问题在于map方法会生成一个Stream。虽然流中的元素是Integer类 型,但Streams接口没有定义sum方法。为什么没有呢?比方说,你只有一个像Transaction那样的 Stream,把所以Transaction加起来是没有任何意义的。但不要担心,Stream API还提供了原始类 型流特化,专门支持处理数值流的方法。
Java 8引入了三个原始类型特化流接口来解决这个问题:IntStream、DoubleStream和 LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到大元素的max。 此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似int和Integer之间的效率差异。
将流转换为特化版本的常用方法是mapToInt、mapToDouble和mapToLong。这些方法和前 面说的map方法的工作方式一样,只是它们返回的是一个特化流,而不是Stream。例如我们可以将例13改造一下,如下所示:

例14
 double statementSum =
                transactions.stream()
                        .mapToDouble(Transaction::getValue)
                        .sum(); // works!

同样,一旦有了数值流,你可能会想把它转换回非特化流。要把原始流转换成一般流(每个double都会装箱成一个 Double),可以使用boxed方法:

DoubleStream doubleStream = transactions.stream()
                .mapToDouble(Transaction::getValue);
        Stream<Double> stream  = doubleStream.boxed();

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

例15
IntStream oddNumbers = 
    IntStream.rangeClosed(10, 30)
             .filter(n -> n % 2 == 1);

构建流(Building Streams)
希望到现在,我们已经让你相信,流对于表达数据处理查询是非常强大而有用的。到目前为 止,你已经能够使用stream方法从集合生成流了。此外,我们还介绍了如何根据数值范围创建 数值流。但创建流的方法还有许多!本节将介绍如何从值序列、数组、文件来创建流,甚至由生成函数来创建无限流!

  • 由值创建流
    你可以使用静态方法Stream.of,通过显式值创建一个流。它可以接受任意数量的参数。例 如,以下代码直接使用Stream.of创建了一个字符串流。然后,你可以将字符串转换为大写,再 一个个打印出来:
例16
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action"); stream.map(String::toUpperCase).forEach(System.out::println); 

你可以使用empty得到一个空流,如下所示:

Stream<String> emptyStream = Stream.empty(); 
  • 由数组创建流
    你可以使用静态方法Arrays.stream从数组创建一个流。它接受一个数组作为参数。例如, 你可以将一个原始类型int的数组转换成一个IntStream,如下所示:
例17
Stream<Integer> numbersFromValues = Stream.of(1, 2, 3, 4);
int[] numbers = {1, 2, 3, 4};
IntStream numbersFromArray = Arrays.stream(numbers);
  • 由文件生成流
    Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。 java.nio.file.Files中的很多静态方法都会返回一个流。例如,一个很有用的方法是 Files.lines,它会返回一个由指定文件中的各行构成的字符串流。使用你迄今所学的内容, 你可以用这个方法看看一个文件中有多少各不相同的词:
例18
long uniqueWords = 0;
        try(Stream<String> lines =  Files.lines(Paths.get(ClassLoader.getSystemResource("data.txt").toURI()), Charset.defaultCharset())){//流会自动 关闭
            uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
                    .distinct()//删除重复项
                    .count();//数一数有多少各不相同的单词

        }
        catch(IOException e){
            e.printStackTrace();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
  • 由函数生成流:创建无限流
    Stream API提供了两个静态方法来从函数生成流:Stream.iterateStream.generate。 这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterategenerate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说, 应该使用*limit(n)*来对这种流加以限制,以避免打印无穷多个值。
    我们先来看一个iterate的简单例子,然后再解释:
例19
Stream.iterate(0, n -> n + 2)
                .limit(10)
                .forEach(System.out::println);

iterate方法接受一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的 Lambda(UnaryOperator类型)。这里,我们使用Lambda n -> n + 2,返回的是前一个元 素加上2。因此,iterate方法生成了一个所有正偶数的流:流的第一个元素是初始值0。然后加 上2来生成新的值2,再加上2来得到新的值4,以此类推。这种iterate操作基本上是顺序的, 因为结果取决于前一次应用。请注意,此操作将生成一个无限流——这个流没有结尾,因为值是 按需计算的,可以永远计算下去。我们说这个流是无界的。正如我们前面所讨论的,这是流和集 合之间的一个关键区别。我们使用limit方法来显式限制流的大小。这里只选择了前10个偶数。 然后可以调用forEach终端操作来消费流,并分别打印每个元素。
与iterate方法类似,generate方法也可让你按需生成一个无限流。但generate不是依次 对每个新生成的值应用函数的。它接受一个Supplier类型的Lambda提供新的值。我们先来 看一个简单的用法:

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

这段代码将生成一个流,其中有五个0到1之间的随机双精度数。例如,运行一次得到了下面 的结果:

0.10697513719728047
0.9488665789337328
0.47239453991978764
0.49255993614793436
0.04943773738322066

Math.Random静态方法被用作新值生成器。同样,你可以用limit方法显式限制流的大小, 否则流将会无限长。 你可能想知道,generate方法还有什么用途。我们使用的供应源(指向Math.random的方 法引用)是无状态的:它不会在任何地方记录任何值,以备以后计算使用。但供应源不一定是无 状态的。你可以创建存储状态的供应源,它可以修改状态,并在为流生成下一个值时使用。
Lambda允许你创建函数式接口的实例,只要直接内联提供方法的实 现就可以。你也可以像下面这样,通过实现IntSupplier接口中定义的getAsInt方法显式传递 一个对象(虽然这看起来是无缘无故地绕圈子,也请你耐心看):

例21
        IntStream twos = IntStream.generate(new IntSupplier(){
            public int getAsInt()
            {
                return 2;
            }
        });
        twos.limit(10).forEach(System.out::println);

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

结论

Java SE 8引入了Streams API,它允许你处理复杂的数据查询。 在本文中,你已经看到流支持许多操作,例如filter,map,reduce和iterate,它们可以组合起来编写简洁和富有表现力的数据处理。 这种编写代码方式与在Java SE 8之前处理集合的方式有很大不同。但是,它有许多好处。 首先,Streams API利用多种技术(如懒惰和短路)来优化您的数据处理查询。 其次,流可以自动并行化以利用多核架构。 在下一篇文章中,我们将探索更高级的操作,例如flatMap和collect。

本章节代码请参见stream example

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值