使用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,未完待续。

版权声明:本文为weixin_36740203原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_36740203/article/details/91888274

智能推荐

RIP/DHCP/ACL综合实验

组播: 加入组的组成员才会接受到消息,只需要将流量发送一次到组播地址 减少控制面流量,减少头部复制, RIP1  广播   有类  不支持认证 RIP2  组播   无类  (支持VLAN)、支持认证 所有距离矢量路由协议:具有距离矢量特征的协议,都会在边界自动汇总 控制平面  路由的产生是控制平面的流量 数据平面  ...

【Sublime】使用 Sublime 工具时运行python文件

使用 Sublime 工具时报Decode error - output not utf-8解决办法   在菜单中tools中第四项编译系统 内最后一项增添新的编译系统 自动新建 Python.sublime-build文件,并添加"encoding":"cp936"这一行,保存即可 使用python2 则注释encoding改为utf-8 ctr...

java乐观锁和悲观锁最底层的实现

1. CAS实现的乐观锁 CAS(Compare And Swap 比较并且替换)是乐观锁的一种实现方式,是一种轻量级锁,JUC 中很多工具类的实现就是基于 CAS 的,也可以理解为自旋锁 JUC是指import java.util.concurrent下面的包, 比如:import java.util.concurrent.atomic.AtomicInteger; 最终实现是汇编指令:lock...

Python 中各种imread函数的区别与联系

  原博客:https://blog.csdn.net/renelian1572/article/details/78761278 最近一直在用python做图像处理相关的东西,被各种imread函数搞得很头疼,因此今天决定将这些imread总结一下,以免以后因此犯些愚蠢的错误。如果你正好也对此感到困惑可以看下这篇总结。当然,要了解具体的细节,还是应该 read the fuc...

用栈判断一个字符串是否平衡

注: (1)本文定义:左符号:‘(’、‘[’、‘{’…… 右符号:‘)’、‘]’、‘}’……. (2)所谓的字符串的符号平衡,是指字符串中的左符号与右符号对应且相等,如字符串中的如‘(&r...

猜你喜欢

JAVA环境变量配置

位置 计算机->属性->高级系统设置->环境变量 方式一 用户变量新建path 系统变量新建classpath 方式二 系统变量 新建JAVA_HOME,值为JDK路径 编辑path,前加 方式三 用户变量新建JAVA_HOME 此路径含lib、bin、jre等文件夹。后运行tomcat,eclipse等需此变量,故最好设。 用户变量编辑Path,前加 系统可在任何路径识别jav...

常用的伪类选择器

CSS选择器众多 CSS选择器及权重计算 最常用的莫过于类选择器,其它的相对用的就不会那么多了,当然属性选择器和为类选择器用的也会比较多,这里我们就常用的伪类选择器来讲一讲。 什么是伪类选择器? CSS伪类是用来添加一些选择器的特殊效果。 常用的为类选择器 状态伪类 我们中最常见的为类选择器就是a标签(链接)上的为类选择器。 当我们使用它们的时候,需要遵循一定的顺序问题,否则将可能出现bug 注意...

ButterKnife的使用介绍及原理探究(六)

前面分析了ButterKnife的源码,了解其实现原理,那么就将原理运用于实践吧。 github地址:       点击打开链接 一、自定义注解 这里为了便于理解,只提供BindView注解。 二、添加注解处理器 添加ViewInjectProcessor注解处理器,看代码, 这里分别实现了init、getSupportedAnnotationTypes、g...

1.写一个程序,提示输入两个字符串,然后进行比较,输出较小的字符串。考试复习题库1|要求:只能使用单字符比较操作。

1.写一个程序,提示输入两个字符串,然后进行比较,输出较小的字符串。 要求只能使用单字符比较操作。 参考代码: 实验结果截图:...

小demo:slideDown()实现二级菜单栏下拉效果

效果如下,鼠标经过显示隐藏的二级菜单栏 但是这样的时候会存在一个问题,就是鼠标快速不停移入移出会导致二级菜单栏闪屏现象,一般需要使用stop()来清除事件  ...