Java8使用Streams处理数据

–使用流操作来表达复杂的数据处理查询。
 没有集合会怎么办?几乎每个Java应用程序都会生成并处理集合。它们是许多编程任务的基础:它们允许您分组和处理数据。例如,您可能需要创建一个集合来表示客户在银行的交易。然后,您可能想要处理整个集合,以了解客户花费了多少钱。尽管它们的重要性,但处理集合在Java方面远非完美。
首先,集合上的典型处理模式类似于SQL的操作,例如“查找”(例如,找到具有最高价值的交易)或“分组”(例如,给杂货购物相关的所有交易分组)。大多数数据库允许您以声明方式指定这样的操作。例如,以下SQL查询允许您查找具有最高值的事务ID:“SELECT id,MAX(value)from transactions”)。
  如您所见,我们不需要实现如何计算最大值(例如,使用循环和变量来跟踪最大值)。我们只表达我们的期望。这个基本思想意味着你不必担心如何明确地实现这样的查询 - 它已经为你处理好。为什么我们不能做类似集合的事情?你有多少次发现自己用一遍又一遍的循环来重新实现这些操作?
第二,如何有效地处理真正的大型集合?理想情况下,要加快处理速度,您希望利用多核架构。然而,编写并行代码是困难和容易出错的。
Java SE 8来解决这个问题! Java API设计人员正在使用称为Stream的新抽象来更新API,从而可以以声明的方式处理数据。此外,流可以利用多核架构,而无需编写单行多线程代码。听起来不错,不是吗?这就是这一系列文章将探讨的。
在我们详细探索可以使用流之前,让我们来看一个例子,以便您了解Java SE 8流的新编程风格。假设我们需要查找杂货类型的所有交易,并返回按交易价值的递减顺序排列的交易ID列表。在Java SE 7中,我们将清单1所示

List<Transaction> groceryTransactions = new Arraylist<>();
for(Transaction t: transactions){
  if(t.getType() == Transaction.GROCERY){
    groceryTransactions.add(t);
  }
}
Collections.sort(groceryTransactions, new Comparator(){
  public int compare(Transaction t1, Transaction t2){
    return t2.getValue().compareTo(t1.getValue());
  }
});
List<Integer> transactionIds = new ArrayList<>();
for(Transaction t: groceryTransactions){
  transactionsIds.add(t.getId());
}
清单1

在Java SE 8中,我们将如清单二所示

List<Integer> transactionsIds = 
    transactions.stream()
                .filter(t -> t.getType() == Transaction.GROCERY)
                .sorted(comparing(Transaction::getValue).reversed())
                .map(Transaction::getId)
                .collect(toList());
清单2

图1说明了Java SE 8代码。首先,我们使用列表中可用的stream()方法从交易列表(数据)获取流。接下来,将多个操作(过滤,排序,映射,收集)链接在一起以形成流水线,这可以被视为形成对数据的查询。
这里写图片描述
图1
那么如何并行化代码呢?在Java SE 8中,很简单:只需使用并行Stream()替换stream(),如清单3所示,Streams API将内部分解您的查询以利用计算机上的多个内核。

List<Integer> transactionsIds = 
    transactions.parallelStream()
                .filter(t -> t.getType() == Transaction.GROCERY)
                .sorted(comparing(Transaction::getValue).reversed())
                .map(Transaction::getId)
                .collect(toList());
清单3

  别担心,这段代码是如何轻松的压倒一切,我们将在接下来探讨它是如何运作。但是,请注意使用lambda表达式(例如,t-> t.getCategory()== Transaction.GROCERY)和方法引用(例如Transaction :: getId),您现在应该熟悉这一点。
现在,您可以将流视为一种抽象,以便在数据集合上表达高效的类SQL操作。此外,这些操作可以使用lambda表达式进行简洁的参数化。
在本系列关于Java SE 8流的文章的最后,您将能够使用Streams API编写类似于清单3的代码来表达强大的查询。
Streams入门:
我们从一点理论开始。流的定义是什么?一个简短的定义是“来自支持聚合操作的源的一系列元素”,我们来解释一下:
● 元素序列:流提供与特定元素类型的有序值集的接口。但是,流实际上并不存储元素;它们是按需计算的。
● 来源:Streams的数据提供源(如集合,数组或I / O资源)。
● 聚合操作:Streams支持类似SQL的操作和功能性编程语言的常见操作,例如 filter, map, reduce, find, match, sorted等。
此外,流操作有两个基本特征,使它们与集合的操作非常不同:
● 流水线:许多流操作本身返回流。这允许操作被链接以形成更大的管道。这使得能够进行某些优化,例如 laziness and short-circuiting,我们以后再探讨。
● 内部迭代:与集合相反,它们被显式迭代(外部迭代),流操作会在您的幕后进行迭代。
我们再来看一下我们早期的代码示例来解释这些想法。图2更详细地说明了清单2。
这里写图片描述
图2
我们首先通过调用stream()方法从交易列表中获取流。数据源是交易列表,并将向流提供一系列元素。接下来,我们在流上应用一系列聚合操作:filter(过滤指定谓词的元素),排序(排序给定比较器的元素)和映射(提取信息)。除了collect之外的所有这些操作都返回一个Stream,以便它们可以被链接以形成一个流水线,这可以被视为源的查询。
在collect 被调用之前,实际上没有工作。执行collect 操作时才开始处理流水线以返回结果(不是Stream的东西,这里是List)。我们现在不用关心collect ;我们将在未来的文章中详细探讨。目前,您可以将collect 作为一种操作,将作为参数的各种配方用于将流的元素累积到摘要结果中。这里,toList()描述了一个将Stream转换成List的配方。
在我们探索流上不同的方法之前,暂停并反思流和集合之间的概念差异是很好的。
流与集合:
现有的Java集合概念和新概念的流都提供了一系列元素的接口。那有什么区别呢?简而言之,集合是关于数据和流是关于计算。
考虑存储在DVD上的电影。这是一个集合(可能是字节或可能是帧 - 我们们这里不用在意),因为它包含整个数据结构。现在考虑在通过互联网流式传输时观看相同的视频。它现在是一个流(字节或帧)。
在最粗俗的术语中,集合和流之间的差异与计算事物有关。集合是一种内存中数据结构,它保存数据结构当前具有的所有值 - 集合中的每个元素必须先被计算才能添加到集合中。相比之下,流是概念上固定的数据结构,其中元素根据需要计算。
使用Collection接口需要用户执行迭代(例如,使用称为foreach的增强型for循环),这被称为外部迭代。
相比之下,Streams库使用内部迭代 - 它为您执行迭代,并且负责将结果流值存储在某处;你只是提供一个功能来说明要做什么。清单4中的代码(带有集合的外部迭代)和清单5(带有流的内部迭代)说明了这一区别。

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

在清单4中,我们显式地迭代事务列表以提取每个事务ID并将其添加到累加器。相比之下,当使用流时,没有明确的迭代。清单5中的代码构建一个查询,其中map操作被参数化以提取事务ID,并且collect操作将生成的Stream转换为List。
你现在应该了解一个流是什么,你可以做什么。现在来看看Stream支持的不同操作,以便您可以表达自己的数据处理查询。
流操作:利用流处理数据
java.util.stream.Stream中的Stream接口定义了许多操作,它们可以分为两类。在图1所示的示例中,您可以看到以下操作:
● filter, sorted, and map 这些可以连接在一起以形成管道
● collect,关闭管道并返回结果
我们把可以连接的流操作称为中间操作。它们可以连接在一起,因为它们的返回类型是Stream。关闭流管道的操作称为终端操作。它们从一个管道产生一个结果,例如List,Integer,甚至是void(任何非Stream类型)。
你可能会想知道为什么这个区别是重要的。那么,在流管道上调用终端操作之前,中间操作不执行任何处理;它们是“懒惰的”,这是因为中间操作通常可以通过终端操作“合并”并被处理成一个单一的过程。

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;
                  })
           .map(n -> {
                    System.out.println("mapping " + n);
                    return n * n;
                  })
           .limit(2)
           .collect(toList());
清单6

例如,考虑清单6中的代码,它从给定的数字列表中计算出两个偶数的数字。您可能会惊讶于它打印以下内容:

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

这是因为limit(2)使用short-circuiting;我们只需要处理流的一部分,而不是所有的流,以返回结果。这与计算使用and运算符链接的大型布尔表达式类似:只要一个表达式返回false,我们可以推断整个表达式是假的,而不计算它。这里,操作limit返回大小为2的流。
此外,操作filter 和map 已经合并在同一个通行证中。
总结一下我们迄今为止所学到的东西,一般来说,使用流程涉及三件事情:
● 用于执行查询的数据源(如集合)
● 一连串的中间操作,形成一条流管道
● 一个终端操作,其执行流管线并产生结果
现在我们来浏览一些关于流的操作。有关更多示例,请参阅完整列表的java.util .stream.Stream接口以及本文末尾的资源。
过滤。有几个操作可以用来从流中过滤元素:
● filter(Predicate):使用谓词(java.util.function.Predicate)作为参数,并返回包含与给定谓词匹配的所有元素的流
● distinct:返回具有唯一元素的流(根据流元素的equals的实现)
● limit(n):返回不超过给定大小n的流
● skip(n):返回一个丢弃前n个元素的流
查找和匹配。常见的数据处理模式是确定某些元素是否匹配给定的属性。您可以使用anyMatch,allMatch和noneMatch操作来帮助您执行此操作。它们都以谓词作为参数,并返回一个布尔值作为结果(因此,它们是终端操作)。例如,您可以使用allMatch来检查事务流中的所有元素的值是否高于100,如清单7所示。

boolean expensive =
    transactions.stream()
                .allMatch(t -> t.getValue() > 100);

清单 7

此外,Stream接口提供了操作findFirst和findAny,用于从流中检索任意元素。它们可以与其他流操作(如过滤器)结合使用。 findFirst和findAny都返回一个可选对象,如清单8所示。

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

Optional类(java.util .Optional)是一个容器类,用于表示值的存在或不存在。在清单8中,findAny可能没有找到任何类型杂货的交易。可选类包含几种方法来测试元素的存在。例如,如果一个交易存在,我们可以选择使用ifPresent方法对可选对象应用一个操作,如清单9所示(我们只打印交易)。

transactions.stream()
              .filter(t -> t.getType() == Transaction.GROCERY)
              .findAny()
              .ifPresent(System.out::println);
清单9

映射。 Streams支持方法映射,它将一个函数(java.util.function.Function)作为参数将流的元素投影到另一个表单中。该函数应用于每个元素,“映射”为新元素。
例如,您可能希望使用它从流的每个元素中提取信息。在清单10的示例中,我们从列表中返回每个单词的长度列表。
Reducing。到目前为止,我们看到的终端操作返回一个布尔值(allMatch等等),void(forEach)或可选对象(findAny等)。我们也一直使用collect将Stream中的所有元素组合成列表。

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

但是,您还可以将流中的所有元素组合以制定更复杂的进程查询,例如“具有最高ID的交易是什么?”或“计算所有交易值的总和”。可以使用reduce对流进行操作,其重复地对每个元素应用操作(例如,添加两个数字),直到产生结果。它通常被称为功能编程中的折叠操作,因为您可以将此操作视为“折叠”一张长纸(流),直到形成一个小方形,这是折叠操作的结果。
它有助于首先看看我们如何使用for循环计算列表的总和:

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

使用加法运算符迭代地组合数字列表中的每个元素以产生结果。我们基本上将“数字”列表“减少”为一个数字。此代码中有两个参数:sum变量的初始值,在这种情况下为0,以及组合列表的所有元素的操作,在本例中为+
在流上使用reduce方法,我们可以总结流的所有元素,如清单11所示。reduce方法有两个参数:
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
清单11
● 一个初始值,这里是0
● 一个二进制操作符组合两个元素并产生一个
reduce方法基本上是抽象重复应用的模式。诸如“计算产品”或“计算最大值”的其他查询(见清单12)成为reduce方法的特殊用例。

int product = numbers.stream().reduce(1, (a, b) -> a * b);
int product = numbers.stream().reduce(1, Integer::max);
清单12

数字流
您刚看到可以使用reduce方法来计算整数流的总和。但是,有一个成本:我们执行许多包装操作,以反复添加Integer对象在一起。如果我们可以调用一个sum方法,如清单13所示,是否更清楚我们的代码的意图呢?

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

Java SE 8引入了三个原始的专用流接口来解决这个问题,即IntStream,DoubleStream和LongStream,它们分别将流的元素专门化为int,double和long。
您将用于将流转换为专门版本的最常用的方法是mapToInt,mapToDouble和mapToLong。这些方法类似于我们之前看到的方法映射,但是它们返回一个专门的流而不是Stream 。例如,我们可以改进清单13中的代码,如清单14所示。您还可以使用包装操作将原始流转换为对象流。

int statementSum = 
    transactions.stream()
                .mapToInt(Transaction::getValue)
                .sum(); // works!
清单14

最后,另一种有用的数字流形式是数字范围。例如,您可能希望生成1到100之间的所有数字。Java SE 8引入了IntStream,DoubleStream和LongStream两种可用的静态方法,以帮助生成范围和范围关闭。
两种方法都将起始值作为第一个参数,范围的结束值作为第二个参数。然而,range是排他的,而rangeClosed是包容性的。清单15是使用rangeClosed返回10到30之间的所有奇数数据流的示例。

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

创建流
有几种方法来构建流。您已经看到了如何从集合中获取流。而且,我们玩数字流。您还可以从值,数组或文件创建流。另外,甚至可以从一个函数生成一个流来产生无限流!
从值或数组创建流是直截了当的:只需使用静态方法Stream .of的值和数组的Arrays.stream,如清单16所示。

Stream<Integer> numbersFromValues = Stream.of(1, 2, 3, 4);
int[] numbers = {1, 2, 3, 4};
IntStream numbersFromArray = Arrays.stream(numbers);
清单16

您也可以使用Files.lines静态方法转换文本流中的文件。例如,在清单17中,我们计算文件中的行数。

long numberOfLines = 
    Files.lines(Paths.get(“yourFile.txt”), Charset.defaultCharset())
         .count();
清单17

无限流。最后,在我们结束关于流的第一篇文章之前,这是一个令人兴奋的想法。现在你应该明白,流的要素是按需生产的。有两种静态方法 -Stream.iterate和Stream .generate - 可以让你从一个函数创建一个流。然而,因为元素是按需计算的,这两个操作可以产生“永远”的元素。这就是我们所说的无限流:一个没有固定大小的流,当我们从一个固定的采集。
清单18是一个使用iterate来创建所有数字的流的示例,它是10的倍数。迭代方法采用一个初始值(这里为0)和一个lambda(类型为UnaryOperator )来连续应用于每个新的产值。

Stream<Integer> numbers = Stream.iterate(0, n -> n + 10);
清单18

我们可以使用限制操作将无限流转换为固定大小的流。例如,我们可以将流的大小限制为5,如清单19所示。

numbers.limit(5).forEach(System.out::println); // 0, 10, 20, 30, 40
清单19

  结论 Java SE 8引入了Streams API,它可以让您表达复杂的数据处理查询。在本文中,您已经看到一个流支持许多操作,例如filter, map, reduce, 和 iterate,可以组合起来写入简洁明了的数据处理查询。这种编写代码的新方法与在Java SE 8之前如何处理集合非常不同。但是它具有许多好处。首先,Streams API使用诸如laziness 和 short-circuiting的几种技术来优化您的数据处理查询。第二,流可以自动并行化以利用多核架构。在本系列的下一篇文章中,我们将探讨更多高级操作,如flatMap和collect。敬请关注。
  Raoul-Gabriel Urma目前正在剑桥大学完成计算机科学博士学位,他在编程语言方面进行研究。此外,他还是Java 8的作者:Lambdas,Streams和功能性编程(Manning,2014)。原文地址:原文地址:http://www.oracle.com/technetwork/articles/java/ma14-java-se-8-streams-2177646.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值