java处理二进制流数据
使用流操作来表达复杂的数据处理查询。
如果没有收藏,您会怎么办? 几乎每个Java应用程序都会创建和处理集合。 它们是许多编程任务的基础:它们使您可以对数据进行分组和处理。 例如,您可能要创建一组银行交易来表示客户的对帐单。 然后,您可能需要处理整个集合,以了解客户花费了多少钱。 尽管它们很重要,但是在Java中处理集合远非完美。
首先,集合上的典型处理模式类似于类似SQL的操作,例如“查找”(例如,找到具有最高价值的交易)或“分组”(例如,将与杂货店购物相关的所有交易分组)。 大多数数据库允许您声明性地指定此类操作。 例如,以下SQL查询使您可以找到具有最高值的事务ID: "SELECT id, MAX(value) from transactions"
。
如您所见,我们不需要实现如何计算最大值(例如,使用循环和变量来跟踪最大值)。 我们只是表达我们所期望的。 这个基本想法意味着您无需担心如何显式实现此类查询,它会为您处理。 为什么我们不能对收藏做类似的事情? 您发现自己一次又一次地使用循环来重新实现这些操作?
其次,我们如何才能有效地处理非常大的收藏集? 理想情况下,要加快处理速度,您想利用多核体系结构。 但是,编写并行代码既困难又容易出错。
这是一个令人赞叹的想法:这两个操作可以“永远”产生元素。
抢救Java SE 8! Java API设计人员正在使用称为Stream的新抽象来更新API,该抽象使您可以以声明的方式处理数据。 此外,流可以利用多核体系结构,而无需编写一行多线程代码。 听起来不错,不是吗? 这就是本系列文章将探讨的内容。
在详细探讨如何使用流之前,让我们看一个示例,以便对Java SE 8流的新编程风格有所了解。 假设我们需要找到grocery
类型的所有交易,并返回以交易价值降序排序的交易ID列表。 在Java SE 7中,我们将如清单1所示。 在Java SE 8中,我们将如清单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());
}
清单1
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代码。 首先,我们使用List
可用的stream()
方法从事务列表(数据)中获得一个流。 接下来,将几个操作( filter
, sorted
, map
, collect
)链接在一起以形成管道,可以将其视为对数据的查询。
图1
那么如何并行化代码呢? 在Java SE 8中,这很容易:只需用parallel 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
)的使用。 (要了解lambda表达式,请参阅以前的Java Magazine文章和本文结尾处列出的其他资源。)
现在,您可以将流视为一种抽象,用于对数据集合表达高效的,类似于SQL的操作。 此外,可以使用lambda表达式简洁地参数化这些操作。
在有关Java SE 8流的系列文章的最后,您将能够使用Streams API编写类似于清单3的代码来表达强大的查询。
流入门
让我们从一些理论开始。 流的定义是什么? 简短的定义是“源中支持聚合操作的一系列元素”。 让我们分解一下:
- 元素序列:流为特定元素类型的序列值集提供接口。 但是,流实际上并不存储元素。 它们是按需计算的。
- 源:流从提供数据的源(例如集合,数组或I / O资源)消耗。
- 聚合操作:流支持类似SQL的操作以及功能性编程语言的常见操作,例如
filter
,map
,reduce
,find
,match
,sorted
等等。
此外,流操作具有两个基本特征,使其与收集操作有很大不同:
- 流水线:许多流操作本身都会返回一个流。 这允许将操作链接在一起以形成更大的管道。 这使某些优化,如懒惰和短路 ,这是我们后来探索。
- 内部迭代:与显式迭代的集合( 外部迭代 )相反,流操作为您在后台进行迭代。
让我们重新访问前面的代码示例以解释这些想法。 图2更详细地说明了清单2 。
图2
我们首先通过调用stream()
方法从事务列表中获得一个流。 数据源是事务列表,并将向流提供一系列元素。 接下来,我们对流应用一系列聚合操作: filter
(过滤给定谓词的元素), sorted
(对给定比较器的元素排序)和map
(提取信息)。 除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
。
现在,您应该对什么是流以及如何使用它有个好主意。 现在,让我们看一下流支持的不同操作,以便您可以表达自己的数据处理查询。
流操作:利用流来处理数据
java.util .stream.Stream
的Stream
接口定义了许多操作,这些操作可以分为两类。 在图1所示的示例中,您可以看到以下操作:
-
filter
,sorted
和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)
使用短路 ; 我们只需要处理部分流,而不是全部,就可以返回结果。 这类似于评估与and
运算符链接的大型布尔表达式:一旦一个表达式返回false
,我们就可以推断出整个表达式为false
而不评估所有表达式。 在此,操作limit
返回大小为2
的流。
Streams API将在内部分解查询,以利用计算机上的多个内核。
此外,操作filter
和map
已合并在同一遍中。
总结到目前为止我们所学的内容,使用流通常涉及三件事:
- 在其上执行查询的数据源(例如集合)
- 一系列中间操作,形成一条流管道
- 一个终端操作,执行流管道并产生结果
现在,让我们看一下流中可用的一些操作。 请参阅java.util .stream.Stream
接口以获取完整列表,以及更多示例请参见本文结尾处的资源。
过滤。 有几种操作可用于从流中过滤元素:
-
filter(Predicate)
:将一个谓词(java.util.function.Predicate
)作为参数,并返回一个流,其中包括与给定谓词匹配的所有元素 -
distinct
:返回具有唯一元素的流(根据stream元素的equals
实现) -
limit(n)
:返回不超过给定大小n
-
skip(n)
:返回一个流,其中前n个元素被丢弃
查找和匹配。 常见的数据处理模式是确定某些元素是否与给定属性匹配。 您可以使用anyMatch
, allMatch
和noneMatch
操作来帮助您完成此操作。 它们都以谓词作为参数,并返回boolean
作为结果(因此,它们是终端操作)。 例如,您可以使用allMatch
来检查事务流中的所有元素的值都大于100,如清单7所示。
boolean expensive =
transactions.stream()
.allMatch(t -> t.getValue() > 100);
清单7
此外, Stream
接口提供了findFirst
和findAny
操作,用于从流中检索任意元素。 它们可以与其他流操作(例如filter
结合使用。 findFirst
和findAny
返回一个Optional
对象,如清单8所示。
Optional<Transaction> =
transactions.stream()
.filter(t -> t.getType() == Transaction.GROCERY)
.findAny();
清单8
Optional<T>
类( java.util .Optional
)是一个容器类,用于表示值的存在或不存在。 在清单8中 , findAny
可能找不到grocery
类型的任何交易。 Optional
类包含几种方法来测试元素的存在。 例如,如果存在事务,我们可以选择使用ifPresent
方法在可选对象上应用操作,如清单9所示(我们仅打印事务)。
transactions.stream()
.filter(t -> t.getType() == Transaction.GROCERY)
.findAny()
.ifPresent(System.out::println);
清单9
映射。 流支持方法map
,该方法采用一个函数( java.util.function.Function
)作为参数,以将流的元素投影为另一种形式。 该功能将应用于每个元素,将其“映射”到一个新元素中。
例如,您可能希望使用它从流的每个元素中提取信息。 在清单10的示例中,我们返回列表中每个单词的长度的列表。 减少。 到目前为止,我们已经看到的终端操作返回一个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());
清单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
-
BinaryOperator<T>
组合两个元素并产生一个新值
reduce
方法从本质上抽象了重复应用程序的模式。 其他查询,例如“计算乘积”或“计算最大值”(请参见清单12 )成为reduce
方法的特殊用例。
int product = numbers.stream().reduce(1, (a, b) -> a * b);
int product = numbers.stream().reduce(1, Integer::max);
清单12
数值流
您已经看到可以使用reduce
方法来计算整数流的总和。 但是,这是有代价的:我们执行许多装箱操作,以将Integer
对象重复添加在一起。 如清单13所示,如果我们可以调用sum
方法来更清楚地了解我们的代码意图,那会更好吗?
int statement =
transactions.stream()
.map(Transaction::getValue)
.sum(); // error since Stream has no sum method
清单13
Java SE 8引入了三个原始的专用流接口来解决此问题: IntStream
, DoubleStream
和LongStream
, LongStream
分别将流的元素专门化为int
, double
和long
。
将流转换为特殊版本的最常用方法是mapToInt
, mapToDouble
和mapToLong
。 这些方法的工作方式与我们之前看到的方法map
完全相同,但是它们返回的是专用流而不是Stream<T>
。 例如,我们可以改进清单13中的代码,如清单14所示。 您还可以使用boxed
操作将原始流转换为对象流。
int statementSum =
transactions.stream()
.mapToInt(Transaction::getValue)
.sum(); // works!
清单14
最后,数字流的另一种有用形式是数字范围。 例如,您可能希望生成1到100之间的所有数字IntStream
8引入了IntStream
, DoubleStream
和LongStream
上可用的两个静态方法来帮助生成这样的范围: range
和rangeClosed
。
两种方法都将范围的起始值作为第一个参数,并将范围的结束值作为第二个参数。 但是, 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.的倍数的所有数字流iterate
方法需要的初始值(在此, 0
)和拉姆达(类型UnaryOperator<T>
在每个新的应用连续产生的价值。
Stream<Integer> numbers = Stream.iterate(0, n -> n + 10);
清单18
我们可以使用limit
操作将无限流转换为固定大小的流。 例如,我们可以将流的大小限制为5,如清单19所示。
numbers.limit(5).forEach(System.out::println); // 0, 10, 20, 30, 40
清单19
结论
Java SE 8引入了Streams API,该API使您可以表达复杂的数据处理查询。 在本文中,您已经看到流支持许多操作,例如filter
, map
, reduce
和iterate
,可以将它们组合在一起以编写简洁明了的数据处理查询。 这种新的代码编写方式与Java SE 8之前的处理集合的方式非常不同。但是,它有很多好处。 首先,Streams API利用懒惰和短路等多种技术来优化数据处理查询。 其次,流可以自动并行化以利用多核体系结构。 在本系列的下一篇文章中,我们将探索更高级的操作,例如flatMap
和collect
。 敬请关注。
最初发表于2014年3月/ 4月的Java Magazine 。 立即订阅 。
Raoul-Gabriel Urma 目前在剑桥大学完成计算机科学博士学位,在那里他从事编程语言研究。 此外,他还是《 Java 8 in Action:Lambda,流和函数式编程》的作者 (Manning,2014年)。
(1)最初发表于Java Magazine 2014年3月/ 4月版
(2)版权所有©[2013] Oracle。
翻译自: https://jaxenter.com/processing-data-with-java-se-8-streams-part-1-107717.html
java处理二进制流数据