java处理二进制流数据_使用Java SE 8流处理数据,第1部分

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()方法从事务列表(数据)中获得一个流。 接下来,将几个操作( filtersortedmapcollect )链接在一起以形成管道,可以将其视为对数据的查询。

流-f1

图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的操作以及功能性编程语言的常见操作,例如filtermapreducefindmatchsorted等等。

此外,流操作具有两个基本特征,使其与收集操作有很大不同:

  • 流水线:许多流操作本身都会返回一个流。 这允许将操作链接在一起以形成更大的管道。 这使某些优化,如懒惰短路 ,这是我们后来探索。
  • 内部迭代:与显式迭代的集合( 外部迭代 )相反,流操作为您在后台进行迭代。

让我们重新访问前面的代码示例以解释这些想法。 图2更详细地说明了清单2

流-f2

图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.StreamStream接口定义了许多操作,这些操作可以分为两类。 在图1所示的示例中,您可以看到以下操作:

  • filtersortedmap ,可以将它们连接在一起以形成管道
  • collect ,它关闭了管道并返回了结果

可以连接的流操作称为中间操作 。 它们可以连接在一起,因为它们的返回类型是Stream 。 关闭流管道的操作称为终端操作 。 它们从诸如ListInteger甚至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将在内部分解查询,以利用计算机上的多个内核。

此外,操作filtermap已合并在同一遍中。

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

  • 在其上执行查询的数据源(例如集合)
  • 一系列中间操作,形成一条流管道
  • 一个终端操作,执行流管道并产生结果

现在,让我们看一下流中可用的一些操作。 请参阅java.util .stream.Stream接口以获取完整列表,以及更多示例请参见本文结尾处的资源。

过滤。 有几种操作可用于从流中过滤元素:

  • filter(Predicate) :将一个谓词( java.util.function.Predicate )作为参数,并返回一个流,其中包括与给定谓词匹配的所有元素
  • distinct :返回具有唯一元素的流(根据stream元素的equals实现)
  • limit(n) :返回不超过给定大小n
  • skip(n) :返回一个流,其中前n个元素被丢弃

查找和匹配。 常见的数据处理模式是确定某些元素是否与给定属性匹配。 您可以使用anyMatchallMatchnoneMatch操作来帮助您完成此操作。 它们都以谓词作为参数,并返回boolean作为结果(因此,它们是终端操作)。 例如,您可以使用allMatch来检查事务流中的所有元素的值都大于100,如清单7所示。

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

清单7

此外, Stream接口提供了findFirstfindAny操作,用于从流中检索任意元素。 它们可以与其他流操作(例如filter结合使用。 findFirstfindAny返回一个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的示例中,我们返回列表中每个单词的长度的列表。 减少。 到目前为止,我们已经看到的终端操作返回一个booleanallMatch等), voidforEach )或一个Optional对象( findAny等)。 我们还一直在使用collectStream所有元素组合到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引入了三个原始的专用流接口来解决此问题: IntStreamDoubleStreamLongStreamLongStream分别将流的元素专门化为intdoublelong

将流转换为特殊版本的最常用方法是mapToIntmapToDoublemapToLong 。 这些方法的工作方式与我们之前看到的方法map完全相同,但是它们返回的是专用流而不是Stream<T> 。 例如,我们可以改进清单13中的代码,如清单14所示。 您还可以使用boxed操作将原始流转换为对象流。

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

清单14

最后,数字流的另一种有用形式是数字范围。 例如,您可能希望生成1到100之间的所有数字IntStream 8引入了IntStreamDoubleStreamLongStream上可用的两个静态方法来帮助生成这样的范围: rangerangeClosed

两种方法都将范围的起始值作为第一个参数,并将范围的结束值作为第二个参数。 但是, 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.iterateStream .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使您可以表达复杂的数据处理查询。 在本文中,您已经看到流支持许多操作,例如filtermapreduceiterate ,可以将它们组合在一起以编写简洁明了的数据处理查询。 这种新的代码编写方式与Java SE 8之前的处理集合的方式非常不同。但是,它有很多好处。 首先,Streams API利用懒惰和短路等多种技术来优化数据处理查询。 其次,流可以自动并行化以利用多核体系结构。 在本系列的下一篇文章中,我们将探索更高级的操作,例如flatMapcollect 。 敬请关注。

最初发表于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处理二进制流数据

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值