使用JAVA SE8 Stream处理数据,Part 1

Part 1 使用JAVA SE8 Stream处理数据

原文Processing Data with Java SE8 Streams,Part 1
使用 stream 操作(operation)来表达复杂数据处理查询。
如果没有 Collection 你会怎么办?几乎每一个Java应用都会生成和处理 Collection 。它们对于许多编程任务来讲是必要的存在:它们让你组织和处理数据。例如,也许你想要创建一个银行交易的Collection 来表示客户的消费清单。继而,也许你想要处理整个 Collection 来找到某个客户花了多少钱。尽管它们的存在是重要的,但Java中的 Collection 处理是非常不完美的。

首先,经典的集合处理模式和SQL操作类似,例如"finding"(如,找到交易的最大值)或者"grouping"(例如,组织出所有的与杂货店购物(grocery shopping)相关的交易)。大多数数据库让你像叙述一样描述具体的操作。例如,下面的SQL查询让你找到交易最大值及其对应的那笔交易的ID:“SELECT id,MAX(value) from transaction”。

正如你看到的,我们不需要去实现怎样计算最大值(如,使用循环和变量来追踪最大值)。我们仅仅表达我们所期望的。这意味着不用太去关心怎样去显式地实现这样的查询——它已经帮你处理了。为什么不对 Collection 做类似的事情呢?你是否意识到你一次又一次的用循环实现这些操作已经多少次了?

第二,我们怎样高效地处理大的 Collection?理想的话,为了加速处理过程,你想要利用多核架构。然而,写并行处理的代码是困难的并且容易出错。

Java SE8 来解决问题!Java API 设计者新抽象出来的Stream 接口可以让你用叙述的方式处理数据。更进一步,stream可以使用多核结构而不用你去写多线程代码。听起来很棒不是吗?这正是这一系列文章要探索的内容。

在我们正式地开始探索可以利用Stream干些什么之前,先看一个例子,让你感受一下用Java SE 8 Stream 编程的新编程风格。我们说我们想要找到grocery(杂货店类型)的所有交易并且按照交易值递减的顺序返回交易ID。在SE7中,我们会像 Listing 1 中那样写。而在Java SE 8中,我们会像 Listing 2 中这样写。


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());
}

Listing 1


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

Listing 2
Figure 1 阐释了Java SE8 的代码。首先,我们使用List中的stream()方法从交易列表中获得一个Stream(数据),接下来,几个操作(filter,sorted,map,collect)被连在了一起形成一个管道,这可以视作形成了对数据的一个查询。
在这里插入图片描述
Figure 1
怎样让代码并行呢?在Java SE8 中,很简单,仅仅使用parallelStream()代替Stream()就可以,参见 Listing 3 ,Streams API 会在内部反编译你的查询去利用你电脑上的多核架构。

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

Listing 3
如果感觉这段代码有点信息量过大,不要紧,我们会在下面的章节中探索它的工作机制。这里注意一下lambda 表达式的使用(如,t->t.example() == Transaction.GROCERY)以及方法的引用(例如,Transaction::getId),这些表达式你目前应该是熟悉的(想要重温lambda表达式,可以参考以前的Java Magazine,以及在这篇文章的结尾列出的其他资源。)

目前为止,你可以看到 Stream 表达上的精简,可以像SQL一样对数据 collection 进行操作。此外,操作通过 lambda 表达式可以被简洁地参数化。

在这个系列结束的时候,你将能够使用Streams API写 Listing 3 类似的代码来表达强大的查询。

Getting Started with Streams

先从一点理论开始。Stream 的定义是什么?一个简短的定义是"来自一个源的支持聚合操作的元素序列。"下面来剖析它:

  • 元素序列(Sequence of elements):Stream 为特定元素类型的序列开了一个接口。但Stream 不会真的存储元素;元素按照需求被计算。
  • 源(Source):Stream 消费数据源中的数据,例如collections,arrays,or I/O 资源。
  • 聚合操作(Aggregate operations):Stream 支持类似SQL一样操作,常用的操作来自函数式编程语言,如 filter,map,reduce,find,match,sorted,以及等等。

更进一步,Stream 操作有两个特点使得它们与 Collection 的操作有根本上的不同:

  • Pipelining:许多流式的操作返回 Stream 本身。这让操作可以被连接到一起组成更大的管道。这实际上带来一定的优化,如惰性(Laziness)以及短路(short-circuiting),稍后我们将探索。
  • Internal iteration:和 Collection 相比, Collection 是显式的迭代(显式迭代),Stream 操作则在幕后为你做了迭代。

现在重新看一下我们早前的代码示例来解释上面这些思想。Figure 2 更细致地阐释了 Listing 2
在这里插入图片描述Figure 2
我们首先通过调用stream()方法从交易清单中获得了一个stream。这里的数据源是一个交易清单将,它为stream提供元素序列。接下来,我们对 stream 应用了一些列的聚合操作:filter (给定一个断言过滤出满足断言元素),sorted (给定一个比较器来对元素进行排序),以及 map (来抽取信息)。除了 collect 的其他操作都返回一个 stream 这样他们可以继续形成一个管道,可以视作对数据源的查询。
在 collect 操作被触发之前什么工作都没有做。 collect 被触发之后将会开始处理这个管道来进一步返回一个结果(结果不是一个stream;这里,是一个List)。现在不要关心 collect 操作;我们将在以后的文章中探索它。现在,你可以把 collect 视作带一个表示收集数据的形式的参数,并让stream中元素按照这个形式去积聚行成一个总结性结果的操作。这里 toList() 描述了将 stream 转换为list的收集形式。

在我们探索可以对 Stream 使用的方法们之前,最好暂停一下,对比一下 Stream 和 Collection 之间的概念差异。

Streams Versus Collections

Java Collection 和新的 Stream 在概念上都提供了一系列元素的接口。 所以它们之间有什么不同呢?简而言之,Collection讲的是数据,Streams讲的是计算。

考虑被存储在DVD上的一个电影。这是一个Collection(类型是字节,或者是帧——我们不关心到底是啥)因为它包含了整个数据结构。现在考虑看流过互联网的相同的视频。它现在是一个Stream(是字节或者是帧类型的)。流式视频播放器只需要从现在观看的内容开始提前下载少数帧,所以你可以从这个stream的开始端播放它的值,即便它大多数值还没有被计算出来。(考虑直播一场足球赛)。

用粗略地术语表达的话,Collection 和 Stream 之间的区别和我们什么时候计算被处理的对象有关。一个 Collection 是一个在内存里的数据结构,它持有数据结构当前拥有的所有值——每一个Collection 中的元素在被加入之前都需要被计算完成。作为对比,Stream 是一个在概念上存在的数据结构,它的元素被按需计算。

使用 Collection 接口需要使用者自己完成迭代(如,使用增强for循环,foreach);这被叫做外部迭代。


List<String> transactionIds = new ArrayList<>(); 
for(Transaction t: transactions){
    transactionIds.add(t.getId()); 
}

Listing 4


List<Integer> transactionIds = ransactions.stream()
                .map(Transaction::getId)
                .collect(toList());

Listing 5
Listing 4 中我们显式地迭代交易清单序列来抽取交易ID并把它放到一个容器中。我们使用Stream的时候,没有显式地迭代。Listing 5 中的代码构建了一个查询,这里 map 操作被参数化了用于抽取交易ID,collect 操作把 Stream 转化成 List。

现在你应该对Stream是什么以及你可以用它来做什么有一个好的认识了。来看一下Stream支持的各种各样的操作,可以用它们来组建自己的数据处理查询。

Stream Operations: Exploiting Streams to Process Data(利用流来处理数据)

java.util.stream.Stream 中的 Stream 接口定义了许多操作,可以被分成两类。在Figure 1中阐释的例子中,你可以见到下面两种操作:

  • filter,sorted,以及map,这些可以被连接起来形成一个管道。
  • collect,将会关闭管道并返回一个值。

可以被连接 Stream 操作被称作中间操作(intermediate operations)。它们可以被串到一起因为它们返回类型都是Stream。关闭一个管道的操作叫做终端操作(terminal operations)。它们从一个管道中生成结果,结果例如一个 List,一个 Integar,甚至是 void(任何non-Stream类型)。

也许你会好奇为什么操作间的区别很重要。好吧,中间操作不会有任何的处理直到该管道的终端操作被触发;它们是"lazy"的。中间操作通常可以“合并”,并通过终端操作处理成单通道。


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());

Listing 6
例如,考虑Listing 6中的代码,它将会从给定的数据清单中计算偶数的平方值。可能你会对下面打印的内容惊讶:

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

这是因为 limit(2) 带来了短路*(short-circuiting*),我们只需要处理stream的部分内容,而不是计算所有的内容来返回一个结果。这和推断用“与”来表达一个大的布尔表达式的结果是类似的。只要一个表达式返回false,我们可以推断整个表达式是false而不用推断出所有的结果。这里操作 limit 返回了大小为2的stream。

此外,filter 操作和 map 已经合并在同一通道中。

总结一下我们目前为止学到的内容,使用 Stream,通常,涉及三件事:

  • 一个可以形成一个查询的数据源(data source,如一个 Collection)
  • 一个中间操作链,它形成了一个Stream 管道。
  • 一个终端操作,它执行 Stream 管道并且生成最终的结果。

现在我们来看一些Stream支持的操作。可以对 list 引用java.util.stream.Stream接口,以及查看这篇文章末尾的资源学习更多的例子。

**Filtering.**有几个可以从Stream中过滤元素的操作

  • filter(Predicate):带一个断言(java.util.function.Predicate)作为一个参数并且返回stream中匹配断言的所有元素。
  • distinct:返回拥有不重复元素的stream(重复的判断标准是stream中元素的equals实现)
  • limit(n):返回一个不多于个数n的stream
  • skip(n):返回丢弃前n个元素的stream

Finding and matching.一个常见的数据处理模式是确定一些元素是否匹配给定的属性。可以使用 anyMatch,allMatch 以及 noneMatch 操作来帮你完成这件事。它们都是带一个断言作为参数并且返回一个 boolean 作为结果(他们因此是,终端操作)。例如,你可以使用 allMatch 来检查交易stream 中所有元素的交易值是否都高于100,见Listing 7

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

Listing 7
此外,Stream 接口提供 findFirst 和 findAny 操作来从stream中检索任何元素。他们可以和其他Stream操作一起使用,例如 filter。findFirst 和 findAny 都返回 Optional 对象,见Listing 8

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

Listing 8
Optional 类 (java.util.Optional) 是一个容器类用来表达值的存在与否。在Listing 8中,可能findAny 不会找到任何类型是grocery的交易。Optional 类包括几个用来测试元素存在与否的方法。例如,如果一个交易是存在的,我们可以通过使用IfPresent方法对optional对象使用一个操作,像Listing 9中那样(这里仅仅是打印出该笔交易)。


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

Listing 9
Mapping. Streams支持 map 方法,它带一个函数(java.util.function.Function)作为入参来把stream中的元素组合成另外一个形式。函数作用于每一个元素,把它 “mapping” 成一个新的元素。

例如,你可能想要用它从一个 stream 抽取信息。在 Listing 10 的例子中,我们返回了单词长度组成的 list。**Reducing.**目前为止,我们见到的终端操作有返回boolean(allMatch等等)的,void(forEach)的,或者Optional对象(findAny等等)的。我们现在使用 collect 来组合stream中的所有元素形成一个list。

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

Listing 10
然而,你也可以组合 stream 中所有的元素来计算更复杂的查询,例如"交易额最高的交易ID是?"或者“计算所有交易值的和”。这可能会对 stream 使用 reduce 操作,它重复的对每一个元素使用一个操作(l例如,对两个数字求和)直到结果产生。这在函数式编程里通常被叫做*fold(折叠)*操作,因为你可以把这个操作视作重复折叠一个长条纸(你的 stream )直到它变成了一个小方块,也就是折叠操作的结果。

我们首先看一下我们怎样用for循环来计算一个list的求和:

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

这个数字list的每个元素通过使用加操作被迭代地整合到一起产生了一个结果。我们从根本上把一个数字 list 缩减成了一个数字。这段代码中有两个参数:sum变量的初始化参数,在这里例子中是0,以及用于组合list元素的操作,这里是+。

对stream使用reduce方法,我们可以对stream中的所有元素求和,就像Listing 11中所示的那样。reduce方法带两个参数:

int sum = numbers.stream().reduce(0, (a, b) -> a + b);

Listing 11

  • 一个初始值,这里是0
  • 一个BinaryOperator 来组合两个元素并生成一个新的值。

reduce 方法抽象出了重复应用的模式。其他查询例如"计算乘积"或者"计算最大值"(请看Lising 12)变成了特殊的使用 reduce 方法的例子。

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

Listing 12

Numeric Streams(数字流)

你已经看到了可以使用 reduce 方法来计算 stream 元素的和。实现求和我们的做法是:对整数对象进行重复的加操作对象加到一起。为了使得我们代码的含义更清楚,如果能直接调用一个 sum 方法是不是更好呢?,就像 Listing 13 那样:

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

Listing 13
Java SE8 引入了三个原生的特殊的 stream 接口来处理这个问题——IntStream,DoubleStream,以及LongStream——分别对于元素是 int,double 和 long 的流。

最普通的把 stream 转换成特殊版本 stream 的方法是用 mapToInt,mapToDouble 和 mapToLong。这些方法的功能和我们之前看到的 map一样,但是他们返回的是具体类型的 stream 而不是一个Stream.l例如,我们改进一下Lisiting 13中的代码(见Listing 14)。你可以把原生的 stream 中转换成对象的 stream 通过使用现成的操作。

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

Listing 14
最终,另一个有用的数字流的形式是数字区间。如,也许你想要生成1和100之间所有的数字。Java SE8 引入两个静态的方法,对IntStream,DoubleStream,和LongStream来说是可用的,可以用来生成这两种区间:range 和 rangeClosed。

这两个方法都是以第一个参数作为区间开始,第二个参数作为区间的结尾。然而 range 是左闭右开区间,而 rangeClosed 是闭区间。Listing 15 是一个使用 rangeClosed 的例子来返回一个 stream中的所有10到30之间的奇数。

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

Listing 15

Building Streams

构建流有这样几个方式。你已经知道怎样从 Collection 中构建一个stream。除此之外,我们还可以处理数字的流。你可以从值,数组,或者是一个文件中得到 stream,你甚至可以 从一个函数生成一个stream,形成无限的流!

从一个值或者从一个数组中创建一个流都是很直接的:只是分别调用静态的方法Stream.of和Arrays.stream就好,就像Listing 16中那样:

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

Listing 16
也可以使用Files.lines静态方法把文件转成一个行 stream组成的stream。例如,Listing 17中我们数了一个文件内容的行数。

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

Listing 17
Infinite streams. 最后,在我们对本篇文章进行总结之前有一个头脑风暴的想法。目前为止你已经理解了 stream 中的元素是按需要产生的。有两个静态的方法——Stream.iterate和Stream.generate——它可以让从函数中创建一个stream。然而,因为元素是按需要被计算的,因此两个操作可以“永远地”产生元素,这就是我们说的一个无限流(infinite stream):一个没有固定大小的stream,就像我们从一个固定的集合中创建的那样。

Listing 18 是一个使用iterate生成10的倍数的数字stream。iterate方法带一个初始参数(value,0)以及一个连续应用于新产生的值的lambda(类型是UnaryOperator)。

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

Listing 18
我们可以使用 limit 将一个无限的stream分成固定大小的 stream。例如,我们可以限制 stream 的大小为5,就像 Listing 19 中那样

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

Listing 19

Conclusion

Java SE8 引入了Streams API,这让你可以表达更复杂的数据处理查询。在这篇文章里,可以看到一个 stream 可以支持多种操作例如filter,map,reduce以及iterate,它们可以被组合起来书写更丰富的数据处理查询。这种新的写代码方式和你在Java SE8之前用集合处理数据十分不同,然而,它有许多好处。首先,Streams API引入了惰性和短路来优化数据查询过程。第二,strreams可以被自动地利用你的多核架构并行化处理。在这个系列的下一章,将会继续讨论更高级的操作,例如flatMap 和 collect,未完待续。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值