乐趣区

关于java:Java开发工程师进阶篇Java8的Stream流使用技巧

作者:幻好

起源:恒生 LIGHT 云社区

什么是流?

流是 Java8 引入的全新概念,它用来解决汇合中的数据,暂且能够把它了解为一种高级汇合。

家喻户晓,汇合操作十分麻烦,若要对汇合进行筛选、投影,须要写大量的代码,而流是以申明的模式操作汇合,它就像 SQL 语句,咱们只需通知流须要对汇合进行什么操作,它就会主动进行操作,并将执行后果交给你,无需咱们本人手写代码。

因而,流的汇合操作对咱们来说是通明的,咱们只需向流下达命令,它就会主动把咱们想要的后果给咱们。因为操作过程齐全由 Java 解决,因而它能够依据以后硬件环境抉择最优的办法解决,咱们也无需编写简单又容易出错的多线程代码了。

流的特点

  1. 只能遍历一次
    咱们能够把流设想成一条流水线,流水线的源头是咱们的数据源 (一个汇合),数据源中的元素顺次被输送到流水线上,咱们能够在流水线上对元素进行各种操作。
    一旦元素走到了流水线的另一头,那么这些元素就被“生产掉了”,咱们无奈再对这个流进行操作。当然,咱们能够从数据源那里再取得一个新的流从新遍历一遍。
  2. 采纳外部迭代形式
    若要对汇合进行解决,则需咱们手写解决代码,这就叫做内部迭代。
    而要对流进行解决,咱们只需通知流咱们须要什么后果,处理过程由流自行实现,这就称为外部迭代。

流的操作品种

流的操作分为两种,别离为两头操作和终端操作。

  1. 两头操作
    当数据源中的数据上了流水线后,这个过程对数据进行的所有操作都称为 ” 两头操作 ”。
    两头操作依然会返回一个流对象,因而多个两头操作能够串连起来造成一个流水线。
  2. 终端操作
    当所有的两头操作实现后,若要将数据从流水线上拿下来,则须要执行终端操作。
    终端操作将返回一个执行后果,这就是你想要的数据。

流的操作过程

应用流一共须要三步:

  1. 筹备一个数据源
  2. 执行两头操作
    两头操作能够有多个,它们能够串连起来造成流水线。
  3. 执行终端操作
    执行终端操作后本次流完结,你将取得一个执行后果。

流的应用

创立流

在应用流之前,首先须要领有一个数据源,并通过 Stream API 提供的一些办法获取该数据源的流对象。数据源能够有多种形式:

1. 汇合

这种数据源较为罕用,通过 stream()办法即可获取流对象:

List<Human> list = new ArrayList<Human>(); 
Stream<Human> stream = list.stream();

2. 数组

通过 Arrays 类提供的动态函数 stream()获取数组的流对象:

String[] names = {"chaimm","peter","john"};
Stream<String> stream = Arrays.stream(names);

3. 值

间接将几个值变成流对象:

Stream<String> stream = Stream.of("chaimm","peter","john");

4. 文件

try(Stream lines = Files.lines(Paths.get(“文件路径名”),Charset.defaultCharset())){// TODO}catch(IOException e){}

5. iterator

创立有限流

Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);

Java7 简化了 IO 操作,把关上 IO 操作放在 try 后的括号中即可省略敞开 IO 的代码。

筛选 filter

filter 函数接管一个 Lambda 表达式作为参数,该表达式返回 boolean,在执行过程中,流将元素逐个输送给 filter,并筛选出执行后果为 true 的元素。

如,筛选出所有学生:

List<Human> result = list.stream().filter(Man::isStudent).collect(toList());

去重 distinct

去掉反复的后果:

List<Human> result = list.stream().distinct().collect(toList());

截取 limit

截取流的前 3 个元素:

List<Human> result = list.stream().limit(3).collect(toList());

映射 map

对流中的每个元素执行一个函数,使得元素转换成另一种类型输入。流会将每一个元素输送给 map 函数,并执行 map 中的 Lambda 表达式,最初将执行后果存入一个新的流中。

如,获取每个人的姓名(实则是将 Human 类型转换成 String 类型):

List<String> result = list.stream().map(Human::getName).collect(toList());

跳过 skip

跳过流的前 3 个元素:

List<Human> result = list.stream().skip(3).collect(toList());

合并 concat

合并 2 个元素:

List<Human> result = Stream.concat(human1, human2).collect(toList());

合并多个流

例:列出 List 中各不相同的单词,List 汇合如下:

List<String> list = new ArrayList<String>();
list.add("zhong");
list.add("ming");
list.add("mao");

思路如下:

首先将 list 变成流:

list.stream();

按空格分词:

list.stream().map(line->line.split(""));

分完词之后,每个元素变成了一个 String[]数组。

将每个 String[] 变成流:

list.stream().map(line->line.split("")).map(Arrays::stream)

此时一个大流外面蕴含了一个个小流,咱们须要将这些小流合并成一个流。

将小流合并成一个大流:用 flatMap 替换方才的 map

list.stream().map(line->line.split("")).flatMap(Arrays::stream)

去重

list.stream().map(line->line.split("")).flatMap(Arrays::stream).distinct().collect(toList());

是否匹配任一元素:anyMatch

anyMatch 用于判断流中是否存在至多一个元素满足指定的条件,这个判断条件通过 Lambda 表达式传递给 anyMatch,执行后果为 boolean 类型。

如,判断 list 中是否有学生:

boolean result = list.stream()
            .anyMatch(Man::isStudent);

是否匹配所有元素:allMatch

allMatch 用于判断流中的所有元素是否都满足指定条件,这个判断条件通过 Lambda 表达式传递给 anyMatch,执行后果为 boolean 类型。

如,判断是否所有人都是学生:

boolean result = list.stream().allMatch(Man::isStudent);

是否未匹配所有元素:noneMatch

noneMatch 与 allMatch 恰恰相反,它用于判断流中的所有元素是否都不满足指定条件:

boolean result = list.stream().noneMatch(Man::isStudent);

获取任一元素 findAny

findAny 可能从流中轻易选一个元素进去,它返回一个 Optional 类型的元素。

Optional<Human> human = list.stream().findAny();

获取第一个元素 findFirst

Optional<Human> human = list.stream().findFirst();

归约

归约是将汇合中的所有元素通过指定运算,折叠成一个元素输入,如:求最值、平均数等,这些操作都是将一个汇合的元素折叠成一个元素输入。

在流中,reduce 函数能实现归约。

reduce 函数接管两个参数:

  1. 初始值
  2. 进行归约操作的 Lambda 表达式

元素求和:自定义 Lambda 表达式实现求和

例:计算所有人的年龄总和

int age = list.stream().reduce(0, (human1,human2)->human1.getAge()+human2.getAge());
  1. reduce 的第一个参数示意初试值为 0;
  2. reduce 的第二个参数为须要进行的归约操作,它接管一个领有两个参数的 Lambda 表达式,reduce 会把流中的元素两两输给 Lambda 表达式,最初将计算出累加之和。

元素求和:应用 Integer.sum 函数求和

下面的办法中咱们本人定义了 Lambda 表达式实现求和运算,如果以后流的元素为数值类型,那么能够应用 Integer 提供了 sum 函数代替自定义的 Lambda 表达式,如:

int age = list.stream().reduce(0, Integer::sum);

Integer 类还提供了 minmax 等一系列数值操作,当流中元素为数值类型时能够间接应用。

数值流的应用

采纳 reduce 进行数值操作会波及到根本数值类型和援用数值类型之间的装箱、拆箱操作,因而效率较低。

当流操作为纯数值操作时,应用数值流能取得较高的效率。

将一般流转换成数值流

StreamAPI 提供了三种数值流:IntStream、DoubleStream、LongStream,也提供了将一般流转换成数值流的三种办法:mapToInt、mapToDouble、mapToLong。

如,将 Human 中的 age 转换成数值流:

IntStream stream = list.stream().mapToInt(Human::getAge);

数值计算

每种数值流都提供了数值计算函数,如 max、min、sum 等。如,找出最大的年龄:

OptionalInt maxAge = list.stream().mapToInt(Human::getAge).max();

因为数值流可能为空,并且给空的数值流计算最大值是没有意义的,因而 max 函数返回 OptionalInt,它是 Optional 的一个子类,可能判断流是否为空,并对流为空的状况作相应的解决。

此外,mapToIntmapToDoublemapToLong进行数值操作后的返回后果别离为:OptionalIntOptionalDoubleOptionalLong

两头操作和收集操作

操作 类型 返回类型 应用的类型 / 函数式接口 函数描述符
filter 两头 Stream<T> Predicate<T> T -> boolean
distinct 两头 Stream<T>
skip 两头 Stream<T> long
map 两头 Stream<R> Function<T, R> T -> R
flatMap 两头 Stream<R> Function<T, Stream<R>> T -> Stream<R>
limit 两头 Stream<T> long
sorted 两头 Stream<T> Comparator<T> (T, T) -> int
anyMatch 终端 boolean Predicate<T> T -> boolean
noneMatch 终端 boolean Predicate<T> T -> boolean
allMatch 终端 boolean Predicate<T> T -> boolean
findAny 终端 Optional<T>
findFirst 终端 Optional<T>
forEach 终端 void Consumer<T> T -> void
collect 终端 R Collector<T, A, R>
reduce 终端 Optional<T> BinaryOperator<T> (T, T) -> T
count 终端 long

Collector 收集

收集器用来将通过筛选、映射的流进行最初的整顿,能够使得最初的后果以不同的模式展示。

collect 办法即为收集器,它接管 Collector 接口的实现作为具体收集器的收集办法。

Collector 接口提供了很多默认实现的办法,咱们能够间接应用它们格式化流的后果;也能够自定义 Collector 接口的实现,从而定制本人的收集器。

归约

流由一个个元素组成,归约就是将一个个元素“折叠”成一个值,如求和、求最值、求平均值都是归约操作。

一般性归约

若你须要自定义一个归约操作,那么须要应用 Collectors.reducing 函数,该函数接管三个参数:

  • 第一个参数为归约的初始值
  • 第二个参数为归约操作进行的字段
  • 第三个参数为归约操作的过程

汇总

Collectors 类专门为汇总提供了一个工厂办法:Collectors.summingInt

它可承受一 个把对象映射为求和所需 int 的函数,并返回一个收集器;该收集器在传递给一般的 collect 办法后即执行咱们须要的汇总操作。

分组

数据分组是一种更天然的宰割数据操作,分组就是将流中的元素依照指定类别进行划分,相似于 SQL 语句中的 GROUPBY

多级分组

多级分组能够反对在实现一次分组后,别离对每个小组再进行分组。

应用具备两个参数的 groupingBy 重载办法即可实现多级分组。

  • 第一个参数:一级分组的条件
  • 第二个参数:一个新的groupingBy 函数,该函数蕴含二级分组的条件

Collectors 类的动态工厂办法

工厂办法 返回类型 用处 示例
toList List<T> 把流中所有我的项目收集到一个 List List<Project> projects = projectStream.collect(toList());
toSet Set<T> 把流中所有我的项目收集到一个 Set,删除反复项 Set<Project> projects = projectStream.collect(toSet());
toCollection Collection<T> 把流中所有我的项目收集到给定的供给源创立的汇合 Collection<Project> projects = projectStream.collect(toCollection(), ArrayList::new);
counting Long 计算流中元素的个数 long howManyProjects = projectStream.collect(counting());
summingInt Integer 对流中我的项目的一个整数属性求和 int totalStars = projectStream.collect(summingInt(Project::getStars));
averagingInt Double 计算流中我的项目 Integer 属性的平均值 double avgStars = projectStream.collect(averagingInt(Project::getStars));
summarizingInt IntSummaryStatistics 收集对于流中我的项目 Integer 属性的统计值,例如最大、最小、总和与平均值 IntSummaryStatistics projectStatistics = projectStream.collect(summarizingInt(Project::getStars));
joining String 连贯对流中每个我的项目调用 toString 办法所生成的字符串 String shortProject = projectStream.map(Project::getName).collect(joining(","));
maxBy Optional<T> 依照给定比拟器选出的最大元素的 Optional,或如果流为空则为 Optional.empty() Optional<Project> fattest = projectStream.collect(maxBy(comparingInt(Project::getStars)));
minBy Optional<T> 依照给定比拟器选出的最小元素的 Optional,或如果流为空则为 Optional.empty() Optional<Project> fattest = projectStream.collect(minBy(comparingInt(Project::getStars)));
reducing 归约操作产生的类型 从一个作为累加器的初始值开始,利用 BinaryOperator 与流中的元素一一联合,从而将流归约为单个值 int totalStars = projectStream.collect(reducing(0, Project::getStars, Integer::sum));
collectingAndThen 转换函数返回的类型 蕴含另一个收集器,对其后果利用转换函数 int howManyProjects = projectStream.collect(collectingAndThen(toList(), List::size));
groupingBy Map<K, List<T>> 依据我的项目的一个属性的值对流中的我的项目作问组,并将属性值作 为后果 Map 的键 Map<String,List<Project>> projectByLanguage = projectStream.collect(groupingBy(Project::getLanguage));
partitioningBy Map<Boolean,List<T>> 依据对流中每个我的项目利用断言的后果来对我的项目进行分区 Map<Boolean,List<Project>> vegetarianDishes = projectStream.collect(partitioningBy(Project::isVegetarian));

转换类型

有一些收集器能够生成其余汇合。比方后面曾经见过的 toList,生成了 java.util.List 类的实例。

还有 toSettoCollection,别离生成 SetCollection 类的实例。

到目前为止,我曾经讲了很多流上的链式操作,但总有一些时候,须要最终生成一个汇合——比方:

  • 已有代码是为汇合编写的,因而须要将流转换成汇合传入;
  • 在汇合上进行一系列链式操作后,最终心愿生成一个值;
  • 写单元测试时,须要对某个具体的汇合做断言。

应用 toCollection,用定制的汇合收集元素

stream.collect(toCollection(TreeSet::new));

还能够利用收集器让流生成一个值。maxByminBy 容许用户按某种特定的程序生成一个值。

数据分区

分区是分组的非凡状况:由一个断言(返回一个布尔值的函数)作为分类函数,它称分区函数。

分区函数返回一个布尔值,这意味着失去的分组 Map 的键类型是 Boolean,于是它最多能够分为两组: true 是一组,false 是一组。

分区的益处在于保留了分区函数返回 true 或 false 的两套流元素列表。

并行流

并行流就是一个把内容分成多个数据块,并用不同的线程别离解决每个数据块的流。最初合并每个数据块的计算结果。

将一个程序执行的流转变成一个并发的流只有调用 parallel() 办法

public static long parallelSum(long n){return Stream.iterate(1L, i -> i +1).limit(n).parallel().reduce(0L,Long::sum);
}

将一个并发流转成程序的流只有调用 sequential() 办法

stream.parallel().filter(...).sequential().map(...).parallel().reduce();

这两个办法能够屡次调用,只有最初一个调用决定这个流是程序的还是并发的。

并发流应用的默认线程数等于你机器的处理器外围数。

通过这个办法能够批改这个值,这是全局属性。

System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "12");

并非应用多线程并行流解决数据的性能肯定高于单线程程序流的性能,因为性能受到多种因素的影响。

如何高效应用并发流的一些倡议:

  1. 如果不确定,就本人测试。
  2. 尽量应用根本类型的流IntStream,LongStream,DoubleStream
  3. 有些操作应用并发流的性能会比程序流的性能更差,比方limitfindFirst 依赖元素程序的操作在并发流中是极其耗费性能的。findAny 的性能就会好很多,应为不依赖程序。
  4. 思考流中计算的性能 (Q) 和操作的性能 (N) 的比照, Q 示意单个解决所需的工夫,N 示意须要解决的数量,如果 Q 的值越大, 应用并发流的性能就会越高。
  5. 数据量不大时应用并发流,性能得不到晋升。
  6. 思考数据结构:并发流须要对数据进行合成,不同的数据结构被合成的性能时不一样的。

流的数据源和可分解性

可分解性
ArrayList 十分好
LinkedList
IntStream.range 十分好
Stream.iterate
HashSet
TreeSet

流的个性以及两头操作对流的批改都会对数据对合成性能造成影响。比方固定大小的流在工作合成的时候就能够平均分配,然而如果有 filter 操作,那么流就不能事后晓得在这个操作后还会残余多少元素。

思考终端操作的性能:如果终端操作在合并并发流的计算结果时的性能耗费太大,那么应用并发流晋升的性能就会得失相当。

退出移动版