JavaBase:流

提出问题

集合是Java中的重要API,但是集合操作却远远算不上完美。

  • 很多业务逻辑都涉及到类似数据库的操作,例如对菜品按类别进行分组。利用SQL可以声明式地进行这些操作,但是在集合当中,我们只能用迭代器进行操作。
  • 要处理大量元素时,为了提高性能需要并行处理,使用多核架构,但是并行比迭代器还要复杂并且难以调试。

概述

是什么

流是JavaAPI的新成员,允许你一声明性的方式处理数据集合(通过查询语句来表达,而不是临时编写一个实现),可以看作是遍历数据集的高级迭代器。流可以透明地并行处理,无需写多线程代码。

分类,各个分类是什么

应用

适用性

  • 对于复杂的集合操作,例如对元素进行分类、筛选等,或者需要以多核架构处理大量元素:
    • 流使得代码以声明性编写,说明想要完成什么而不是如何实现一个操作(if或者for),可以轻松应对变化的需求。
    • 可以将几个基础操作链接起来,表达复杂的数据处理流水线,使得代码清晰可读。

优点

  • 声明性。更简洁易读。
  • 可符合。更灵活。
  • 可并行。性能更好。

应用场景

实际案例

使用集合进行对菜品进行按照分类筛选

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<Dish> lowCaloricDishes = new ArrayList<>(); 
for(Dish d: menu){
if(d.getCalories() < 400){
lowCaloricDishes.add(d);//累加器筛选元素
}
}
Collections.sort(lowCaloricDishes, new Comparator<Dish>() {//进行排序
public int compare(Dish d1, Dish d2){
return Integer.compare(d1.getCalories(), d2.getCalories());
}
});
List<String> lowCaloricDishesName = new ArrayList<>();
for(Dish d: lowCaloricDishes){
lowCaloricDishesName.add(d.getName()); //处理后的菜单
}

在上述案例当中,有一个中介变量lowCaloricDishes,其唯一作用就是作为一次中介容器。

使用流处理

以流水线的方式进行表达复杂的数据处理:

  • filter的结果传递给sorted。
  • sorted的结果传递给了map。
  • map的结果传递到了collect。
1
2
3
4
5
6
7
8
import static java.util.Comparator.comparing; 
import static java.util.stream.Collectors.toList;
List<String> lowCaloricDishesName =
menu.stream()
.filter(d -> d.getCalories() < 400) //选出400以下的菜品
.sorted(comparing(Dish::getCalories)) //根据某个值进行排序
.map(Dish::getName) //提取菜的名称
.collect(toList()); //将其保存到list当中

使用多核架构处理,只需要将stream换成parallelStream。

1
2
3
4
5
menu.parallelStream()                    
.filter(d -> d.getCalories() < 400) //选出400以下的菜品
.sorted(comparing(Dish::getCalories)) //根据某个值进行排序
.map(Dish::getName) //提取菜的名称
.collect(toList()); //将其保存到list当中

流简介

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

  • 元素序列。像集合一样,流也提供了一个接口可以访问特定元素类需的一组有序值。
    • 集合是数据结构,其主要目的是以特定的时间/空间复杂度存储和访问元素。
    • 流在与表达计算,如filter、sort、map。即集合讲数据,流讲计算。
  • 源。流会使用一个提供数据的源,如集合、数组或输入/输出资源。从有序集合生成流时会保留原有的顺序。
  • 数据处理操作。流的数据处理支持类似于数据库的操作,以及函数式编程语言中的常用操作,流操作可以顺序也可以并行。

流拥有两个重要特点:

  • 流水线。很多流操作本身会返回一个流,使得多个操作可以连接起来,形成一个大的流水线。
  • 内部迭代。流的迭代操作时在背后进行的。

流与集合

考虑将一个电影作为一个集合,因为它包含了整个数据结构,再考虑通过视频流观看电影,则流媒体只需要提前下载用户观看位置的那几帧即可,而不用将流中的大部分值计算出来,就可以观看流了。

  • 差异就在于什么时候进行计算
    • 集合是一个内存中的数据结构,包含数据结构中目前所有的值。每个元素都得先算出来才能添加到集合中。无论什么时候集合中的元素都是放在内存中的,即使你可以在里面增加\删除元素
    • 流则是在概念上固定的数据结构(不能增删),元素是按需计算的。流可以使得仅仅提取所需要的值,即一个延迟创建的集合。

考虑另一个概念即搜索,如果使用集合,则需要将所有的数据存储进去,而流就可以先读取10个,当用户需要点下一页时再计算接下来10个的值。

流只能遍历一次

流与迭代器一样只能遍历一次,当遍历完之后即这个流已经被消费掉了。当然如果时遍历集合的话,你还可以去生成一个新的流去遍历,但它已经是新的流了。

外部迭代与内部迭代

  • 外部迭代:使用Collection接口需要用户去迭代(for-each等)。
  • 内部迭代:Streams库,它将帮助你完成迭代,并且将流的值存在了某个地方,你只需要给出一个函数说要做什么即可。
    • 项目可以透明地并行处理,或者用更优化的顺序进行处理。使用外部迭代时则需要自己管理所有并行问题。

流操作

java。util.stram.Stram中的Stream接口定义了许多操作,基本可以分为两大类:

  • 中间操作(流水线操作):filter、map、limit可以连成一条流水线。
  • 终端操作(触发或关闭流水线):collect。
1
2
3
4
5
6
List<String> lowCaloricDishesName =                
menu.stream()
.filter(d -> d.getCalories() < 400) //选出400以下的菜品
.sorted(comparing(Dish::getCalories)) //根据某个值进行排序
.map(Dish::getName) //提取菜的名称
.collect(toList()); //将其保存到list当中

对于中间操作会返回一个流,让多个操作可以连接起来形成一个查询。并且除非流水线上触发一个终端操作,否则中间操作不会执行任何处理,他们会很懒,并且一般可以合并到终端操作时一次执行。

对于终端操作,它会从流的流水线生成结果,并且生成不是流的值,例如List等。

使用流

流的使用包括三件事:

  • 一个数据源来执行一个查询。
  • 一个中间操作链,形成一条流的流水线。
  • 一个终端操作,执行流水线,并能生成结果。

流水线背后的理念类似于构建器模式,再构建器中有一个调用链来设置一套配置,之后调用build方法。

Stream支持许多操作,能够让你快速完成复杂的数据查询,如筛选、切片、映射、查找、匹配和归约。

函数介绍

中间操作:

  • 筛选
    • filter。接受lambda,返回Stream。从流中排除某些元素
  • 映射
    • map。接受一个lambda,返回Stream。将元素转换成为其他形式或提取信息(例如提取对象的Name)
    • floatMap。扁平化为一个流,将数组流的内容映射成为一个流
  • 切片
    • limit。返回Stream。截断流,使得元素不超过给定数量
  • sorted。返回Stream。
  • distinct。返回Stream。

终端操作

  • forEach。返回void。消费流中的每个元素并对其应用Lamda

  • count。返回long。返回流中元素的个数

  • 归约。

    • collect。将流转换为一个其他形式,例如list
    • reduce。进行元素计算,例如求和、最大值等
      • 接受参数(初始值,BinaryOperator<T>),返回初始值类型
      • 接受参数(BinaryOperator<T>),返回Optional
  • 匹配

    • anyMatch。返回Boolean。流中是否有一个元素能匹配给定的谓词
    • allMatch。返回Boolean。检查谓词是否匹配所有元素
    • noneMatch。返回Boolean。确保流中没有任何元素与给定谓词匹配
  • 查找

    • findFirst。返回类型是Optional<T>,返回第一个值。findAny在并行运行下返回的可能并不是真正的第一个值

    • findAny。返回类型是Optional<T>,Optional是容器类代表一个值存在或者不存在。返回当前流中的任意元素。可以与filter等配合使用,会再找到第一个值时立即返回

筛选和切片

如何选择流中的元素:用谓词筛选,筛选出不同的元素,忽略流中的头几个元素或将流截短至指定长度。

筛选

stream支持filter方法,该操作接受一个谓词(返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流

筛选各异的元素

流支持一个distinct方法,它会返回一个元素各异(根据流所生成元素的hashcode和equals方法实现)的流。即确保没有重复

截断流

流支持limit方法,返回一个不超过给定长度的流,如果流时有序的,则最多返回前n个元素。

若用于无序流,例如set,则结果不会以任何顺序排列。

跳过元素

流支持skip方法,返回一个扔掉了前n个元素的流,如果流中元素不足n个,则返回一个空流

映射

一个常见的数据处理的套路就是从某些对象中选择信息,例如再SQL中可以从表中选择一列,Stream通过map和floatMap提供了类似的工具

对流中的每一个元素应用函数

流支持map方法,会接受一个函数作为参数,这个函数会被应用到每个元素上,并将其映射成一个的元素。下面的例子将单词流映射成为了单词长度流

1
2
List<String> dishNameLength=menu.Stream()
,map(String::length)

流的扁平化

对于一张单词表,如何列出一张列表,列出里面各不相同的字符呢?第一个例子可能时

1
2
3
4
words.stream()
.map(word->word.split(" "))
.distinct()
.collect(toList());

但这个方法中传给map的方法实际上时返回了一个String[],因此map返回的流就是Stream<String[]>,而不是Stream<String>。

解决方案

  • 尝试使用Map与Arrays.stream

首先我们需要一个字符流,而不是数组流。Arrays.toStream()可以接受一个数组并产生一个流,我们获得的时一个String[] arrayOfWords,将其转换为一个流,即Arrays.stream(arrayOfWords)

1
2
3
4
5
words.stream()
.map(word->word.split(" "))
.map(Arrays::stream)
.distinct()
.collect(toList());

此时得到的时一个流的列表,而不是一个单独的流,因此依然不可行

  • 使用flatMap解决
1
2
3
4
5
words.stream()
.map(word->word.split(" "))
.flatMap(Arrays::stream)
.distinct()
.collect(toList());

flatMap使得各个数组并不是分别映射成一个流,而是映射成流的内容,将多个流合并起来,扁平化成为一个流。

查找和匹配

查看数据集中的某些元素是否匹配一个给定的属性,Stream提供allMatch、anyMatch、noneMatch、findFirst、findAny方法提供了这样的工具。

归约

如何把一个流中的元素组合起来,使用reduce操作表达更复杂的查询,例如计算整个菜单的总卡路里、找到值最大的那一个等。此类查询需要将流中的元素反复结合起来。这样的查询可以被归类为归约操作。

元素求和

假设有一个numbers数组,则求和为:

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

特殊的流

考虑一些特殊的流:数值流、来自文件和数组等多种来源的流、无限流

数值流

构建流

可以通过stream从集合生成流,还可以根据数值范围创建数值流。并且可以从值序列、数组、文件创建流

由值创建流

静态方法Stream.of(任意参数)

由数组创建流

Arrays.stream(数组)

由文件生成流

用流收集数据

收集器简介

归约和汇总

分组

分区

收集器接口

并行数据处理

进阶

参考