Java中的流

83 阅读8分钟

什么是流

流是Java API的新成员,使用流能够以声明性方式处理数据集合,可以透明地并行处理数据,也可以将操作进行复合,使之更加灵活。一个对流的简短定义如下

从支持数据处理操作的源生成的元素序列

可以理解为流像集合一样提供一个访问有序的元素序列的接口。这个序列从一个数据源获得,流支持对元素进行操作,主要目的在于表达计算

流与集合的区别

流和集合都可以表示一组元素,但二者所表达的意义是不同的。

获得元素

  • 集合是内存中的数据结构,每个元素都要先计算出来才能添加到集合中,可以向集合中增加或删除

  • 流可以是无限的,可以按需计算,生产和取用类似生产者-消费者的关系

可以简单理解为集合是大小固定的篮子,流是水龙头。我们可以向篮子中放入和拿走苹果,苹果的个数一定是有限的,而对于水龙头只能接收流出的水,并且使用时不会知道水什么时候会流完。

遍历方式

Stream库为流的遍历提供了内部迭代

相比于集合每次都需要外部迭代,流的迭代已经被完美的封装在内部。无需使用iterator等操作获得迭代器,当需要对元素进行操作时,Stream的内部迭代会将数据自己一个一个的流出。并且会自动选择一种适合硬件的数据表示和并行实现。

流的操作

操作方法

对流进行操作指的是对流中的数据进行处理,根据其处理后的结果是否还是流将其分为中间操作终端操作。流的使用一般包含三件事:

  1. 数据源来执行查询

  2. 中间操作链形成流水线

  3. 终端操作执行流水线生成结果

中间操作

中间操作将一个流转化为另一个流,中间操作可以将对流的处理形成流水线,当接收到终端操作的命令后,流水线就开始工作。中间操作由Stream进行管理,会在必要的时候进行循环合并以优化代码。另外中间操作有短路技巧,即当获得所需的值后就结束计算,这也避免了不必要的操作。

终端操作

终端操作会从流的流水线生成结果,这个结果不是流。流水线中必须有一个终端操作,中间操作才会正常执行,流水线的构建类似builder模式,中间操作链是一系列的设置,终端操作才会触发最终的build方法

使用流的方式

筛选和切片

筛选和切片是中间操作,即从一个流生成另一个流,提供了谓词筛选、去重、截短、跳过元素等操作

  • 使用谓词进行筛选:即使filter方法,可以接收谓词做参数,使用Predicate函数接口进行筛选

  • 去重操作:distinct方法,筛选出各异的元素

  • 截短:当只需要k个元素,使用limit(k)方法最多返回前k个元素

  • 跳过元素:使用skip(k)可以跳过流的前k个元素

映射

映射是将一种流转换成其他类型的流,使用map方法接收Function函数接口。例如将表示重量的Integer类型的流转为Apple流:weights.stream().map(Apple::new).collect(Collectors.toList());。另外流还提供一种扁平化流的方法:flatMap,该方法可以将流中的每个值都转换为一个流,然后把所有流合并为一个流。一个典型的用法是我们使用文件来生成流时,从文件中按行读取数据,然后将行的流转为单词的流。经常与之搭配的是Arrays.stream()操作,该方法可以接受一个数组转换为一个流


// 流中的元素是行

Stream<String> lines = Files.lines(Paths.get(PATH), Charset.defaultCharset());

// flatMap将各个元素拍平,合并为一个流

long uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" "))).distinct().count();

查找和匹配

Match查看数据集中某些元素是否匹配一个给定的属性,这类方法输入一个流返回boolean值,是一个终端操作。匹配操作的参数是一个谓词,以这个谓词进行匹配;

Find从流中获得一个元素,操作通常与其他流操作结合使用,例如findAny()、findFirst()等方法。这类方法返回Option类

另外查找和匹配都是短路操作,一旦得到结果就会结束对流的继续计算,将无限流变为了有限流。

  • allMatch:检查是否流中的所有元素都满足谓词条件

  • anyMatch:检查流中是否有一个元素满足谓词条件

  • noneMatch:allMatch的对立面,流中所有元素都不满足

  • findAny:返回当前流中任意元素

  • findFirst:按照出现顺序,查找流中的第一个元素

关于Optional类

Java8引入的Optional来可以可看作特例模式,是对 null 的改进,避免空指针造成诸多异常。Optional是一个容器类,用来表示一个值存在与否,使用find方法时,可能什么也找不到,为了避免返回null带来的问题,使用Optional来解决,他迫使开发者显式地检查值是否存在和对值不存在时进行处理

  • isPresent() 当Optional包含值时返回true

  • ifPresent(Consumer block) 当Optional包含值时执行代码块

  • get() 返回值或抛出异常

  • orElse(T default) 返回值或返回默认值

归约

归约是对流中的所有元素处理后得到一个最终结果,例如求和、查找最大/小值等。归约是一种有状态的操作,需要一个状态变量对之前的结果进行记录,就像是将一个长纸条折叠成一个小方块一样,是一种累加操作,主要是reduce方法

  • T reduce(T identity, BinaryOperator accumulator); 使用BinaryOperator指定的计算规则对流中的元素进行归约计算,需要设定初始值identity

  • Optional reduce(BinaryOperator accumulator); 与上一个不同的是,没有指定初始值,这就需要考虑流为空时,什么都不会返回的情况,因此规定返回值为Optional

  • U reduce(U identity, BiFunction<U, ? super T, U> accumulator,BinaryOperator combiner); 这种没有严格将元素类型与返回值类型绑定,将其拆分成计算操作和转换操作

归约的优势

与我们手动进行元素的叠加不同,使用归约时完全是元素进行内部迭代,由程序内部进行调度,完成并行化等操作。

数值流

从API中Stream定义可以发现元素都是对象,这样并不利于原始类型的计算:装箱拆箱操作的耗费是不值得的。因此针对原始类型进行了流的特化,使用IntStream、DoubleStream、LongStream三个接口来解决这个问题,分别将流中的元素特化为int、double和long,从而避免了装箱拆箱的成本。

数值流和对象流的相互转化

  • 对象流转化为数值流

使用mapToInt、mapToDouble、mapToLong三个方法完成对象流向三个数值流的转化

  • 数值流转化为对象流

使用**boxed()**方法将特化流转换为对象流

Optional的特化

为了适应流的特化,Optional类也进行了特化。例如使用特化流的求和方法时


OptionInt sum = students.stream().mapToInt(Stundent::getScore).sum();

假如students为空,应该返回什么呢?由于Optional需要将其进行装箱操作,因此有了特化后的OptionalInt。同样存在OptionDouble和OptionLong

构建流

创建流的方法有多种,包括:由值创建,由数组创建,由文件生成,由函数生成几种

由值创建流

Stream类本身提供了一些静态方法用来创建流

  • Stream.of(T ...values) 来从数组中获得流

  • Stream.empty() 创建一个空流

数组创建流

数组工具类Arrays中也提供了从数组创建一个流的方法 Arrays.stream(T[] array)


int sum = Arrays.stream(numbers).sum(); // 计算int[] numbers中元素的和

文件生成流

从文件中生成流可以利用NIO中的处理文件的方法,java.nio.file.Files 中很多静态方法都可以返回一个流,例如获得某个目录下所有文件的路径流、获得文件中每行数据组成流等。其中常用lines方法获得行流后再将其拍扁获得单词流

函数生成流

Stream API 提供了两个静态方法来从函数生成流:Stream.iterate()和Stream.generate()。需要注意这两个操作生成的都是无限流,应使用截短操作加以限制。

迭代操作

iterate(final T seed, final UnaryOperator f) 使用iterate方法生成流是通过迭代操作来完成的,第一个参数是初始值,第二个参数是操作。迭代操作会利用生成的值来获得新值,例如下面生成偶数流的迭代操作


Stream.iterate(0, n -> n + 2).mapToInt(); // 每次在生成值的基础上+2

生成操作

generate(Supplier s) 使用generate方法生成的流是通过Supplier参数生成元素。关键在于Supplier的设计,例如获得偶数流,这就需要使用变量记住上一次的状态


IntSupplier evenSupplier = new IntSupplier() {

    private int num = -2;

    public int getAsInt() {

        num = num + 2;

        return num;

    }
}