简洁又快速地处理集合——java8 Stream(下)

上一篇文章我讲解 Stream 流的基本原理,以及它的基本方法使用,本篇文章咱们继续讲解流的其余操做程序员

值得注意的是:学习 Stream 以前必须先学习 lambda 的相关知识。本文也假设读者已经掌握 lambda 的相关知识。编程

本篇文章主要内容:数组

  1. 一种特化形式的流——数值流
  2. Optional 类
  3. 如何构建一个流
  4. collect 方法
  5. 并行流相关问题

一. 数值流安全

前面介绍的如微信

int sum = list.stream().map(Person::getAge).reduce(0, Integer::sum);
复制代码

计算元素总和的方法其中暗含了装箱成本,map(Person::getAge) 方法事后流变成了 Stream类型,而每一个 Integer 都要拆箱成一个原始类型再进行 sum 方法求和,这样大大影响了效率。数据结构

针对这个问题 Java 8 有良心地引入了数值流 IntStream, DoubleStream, LongStream,这种流中的元素都是原始数据类型,分别是 int,double,longdom

  1. 流与数值流的转换

流转换为数值流函数式编程

  • mapToInt(T -> int) : return IntStream函数

  • mapToDouble(T -> double) : return DoubleStream性能

  • mapToLong(T -> long) : return LongStream

    IntStream intStream = list.stream().mapToInt(Person::getAge);

固然若是是下面这样便会出错

LongStream longStream = list.stream().mapToInt(Person::getAge);
复制代码

由于 getAge 方法返回的是 int 类型(返回的若是是 Integer,同样能够转换为 IntStream)

数值流转换为流

很简单,就一个 boxed

Stream<Integer> stream = intStream.boxed();
复制代码
  1. 数值流方法

下面这些方法做用不用多说,看名字就知道:

  • sum()
  • max()
  • min()
  • average() 等...
  1. 数值范围

IntStream 与 LongStream 拥有 range 和 rangeClosed 方法用于数值范围处理

  • IntStream : rangeClosed(int, int) / range(int, int)
  • LongStream : rangeClosed(long, long) / range(long, long)

这两个方法的区别在于一个是闭区间,一个是半开半闭区间:

  • rangeClosed(1, 100) :[1, 100]
  • range(1, 100) :[1, 100)

咱们能够利用 IntStream.rangeClosed(1, 100) 生成 1 到 100 的数值流

//求 1 到 10 的数值总和:
IntStream intStream = IntStream.rangeClosed(1, 10);
int sum = intStream.sum();
复制代码

二. Optional 类

NullPointerException 能够说是每个 Java 程序员都很是讨厌看到的一个词,针对这个问题, Java 8 引入了一个新的容器类 Optional,能够表明一个值存在或不存在,这样就不用返回容易出问题的 null。以前文章的代码中就常常出现这个类,也是针对这个问题进行的改进。

Optional 类比较经常使用的几个方法有:

  • isPresent() :值存在时返回 true,反之 flase
  • get() :返回当前值,若值不存在会抛出异常
  • orElse(T) :值存在时返回该值,不然返回 T 的值

Optional 类还有三个特化版本 OptionalInt,OptionalLong,OptionalDouble,刚刚讲到的数值流中的 max 方法返回的类型即是这个

Optional 类其中其实还有不少学问,讲解它说不定也要开一篇文章,这里先讲那么多,先知道基本怎么用就能够。

三. 构建流

以前咱们获得一个流是经过一个原始数据源转换而来,其实咱们还能够直接构建获得流。

  1. 值建立流

Stream.of(T...) : Stream.of("aa", "bb") 生成流

//生成一个字符串流
Stream<String> stream = Stream.of("aaa", "bbb", "ccc");
复制代码

Stream.empty() : 生成空流

  1. 数组建立流

根据参数的数组类型建立对应的流:

  • Arrays.stream(T[ ])
  • Arrays.stream(int[ ])
  • Arrays.stream(double[ ])
  • Arrays.stream(long[ ])

值得注意的是,还能够规定只取数组的某部分,用到的是Arrays.stream(T[], int, int)

//只取索引第 1 到第 2 位的:
int[] a = {1, 2, 3, 4};
Arrays.stream(a, 1, 3).forEach(System.out :: println);
//打印 2 ,3
复制代码
  1. 文件生成流

    Stream stream = Files.lines(Paths.get("data.txt"));

每一个元素是给定文件的其中一行

  1. 函数生成流

两个方法:

  • iterate : 依次对每一个新生成的值应用函数

  • generate :接受一个函数,生成一个新的值

    Stream.iterate(0, n -> n + 2) //生成流,首元素为 0,以后依次加 2 Stream.generate(Math :: random) //生成流,为 0 到 1 的随机双精度数 Stream.generate(() -> 1) //生成流,元素全为 1

四. collect 收集数据

coollect 方法做为终端操做,接受的是一个 Collector 接口参数,能对数据进行一些收集归总操做

  1. 收集

最经常使用的方法,把流中全部元素收集到一个 List, Set 或 Collection 中

  • toList

  • toSet

  • toCollection

  • toMap

    List newlist = list.stream.collect(toList()); //若是 Map 的 Key 重复了,但是会报错的哦 Map<Integer, Person> map = list.stream().collect(toMap(Person::getAge, p -> p));

  1. 汇总

(1)counting

用于计算总和:

long l = list.stream().collect(counting());
复制代码

没错,你应该想到了,下面这样也能够:

long l = list.stream().count();
复制代码

推荐第二种

(2)summingInt ,summingLong ,summingDouble

summing,没错,也是计算总和,不过这里须要一个函数参数

计算 Person 年龄总和:

int sum = list.stream().collect(summingInt(Person::getAge));
复制代码

固然,这个能够也简化为:

int sum = list.stream().mapToInt(Person::getAge).sum();
复制代码

除了上面两种,其实还能够:

int sum = list.stream().map(Person::getAge).reduce(Interger::sum).get();
复制代码

推荐第二种

因而可知,函数式编程一般提供了多种方式来完成同一种操做

(3)averagingInt,averagingLong,averagingDouble

看名字就知道,求平均数

Double average = list.stream().collect(averagingInt(Person::getAge));
复制代码

固然也能够这样写

OptionalDouble average = list.stream().mapToInt(Person::getAge).average();
复制代码

不过要注意的是,这两种返回的值是不一样类型的

(4)summarizingInt,summarizingLong,summarizingDouble

这三个方法比较特殊,好比 summarizingInt 会返回 IntSummaryStatistics 类型

IntSummaryStatistics l = list.stream().collect(summarizingInt(Person::getAge));
复制代码

IntSummaryStatistics 包含了计算出来的平均值,总数,总和,最值,能够经过下面这些方法得到相应的数据

getAverage()                                         double
getCount()                                           long
getMax()										 int
getMin()										 int
getSum()										 long
复制代码
  1. 取最值

maxBy,minBy 两个方法,须要一个 Comparator 接口做为参数

Optional<Person> optional = list.stream().collect(maxBy(comparing(Person::getAge)));
复制代码

咱们也能够直接使用 max 方法得到一样的结果

Optional<Person> optional = list.stream().max(comparing(Person::getAge));
复制代码
  1. joining 链接字符串

也是一个比较经常使用的方法,对流里面的字符串元素进行链接,其底层实现用的是专门用于字符串链接的 StringBuilder

String s = list.stream().map(Person::getName).collect(Collectors.joining());
//结果:jackmiketom
String s = list.stream().map(Person::getName).collect(Collectors.joining(","));
//结果:jack,mike,tom
复制代码

joining 还有一个比较特别的重载方法:

String s = list.stream().map(Person::getName).collect(Collectors.joining(" and ", "Today ", " play games."));
//结果:Today jack and mike and tom play games.
复制代码

即 Today 放开头,play games. 放结尾,and 在中间链接各个字符串

  1. groupingBy 分组

groupingBy 用于将数据分组,最终返回一个 Map 类型

Map<Integer, List<Person>> map = list.stream().collect(groupingBy(Person::getAge));
复制代码

例子中咱们按照年龄 age 分组,每个 Person 对象中年龄相同的归为一组

另外能够看出,Person::getAge 决定 Map 的键(Integer 类型),list 类型决定 Map 的值(List类型)

多级分组

groupingBy 能够接受一个第二参数实现多级分组:

Map<Integer, Map<T, List<Person>>> map = list.stream().collect(groupingBy(Person::getAge, groupingBy(...)));
复制代码

其中返回的 Map 键为 Integer 类型,值为 Map<t, list

按组收集数据

Map<Integer, Integer> map = list.stream().collect(groupingBy(Person::getAge, summingInt(Person::getAge)));
复制代码

该例子中,咱们经过年龄进行分组,而后 summingInt(Person::getAge)) 分别计算每一组的年龄总和(Integer),最终返回一个 Map

根据这个方法,咱们能够知道,前面咱们写的:

groupingBy(Person::getAge)
复制代码

其实等同于:

groupingBy(Person::getAge, toList())
复制代码
  1. partitioningBy 分区

分区与分组的区别在于,分区是按照 true 和 false 来分的,所以partitioningBy 接受的参数的 lambda 也是 T -> boolean

//根据年龄是否小于等于20来分区
Map<Boolean, List<Person>> map = list.stream()
                                    .collect(partitioningBy(p -> p.getAge() <= 20));
//打印输出
{
   false=[Person{name='mike', age=25}, Person{name='tom', age=30}], 
   true=[Person{name='jack', age=20}]
}
复制代码

一样地 partitioningBy 也能够添加一个收集器做为第二参数,进行相似 groupBy 的多重分区等等操做。

五. 并行

咱们经过 list.stream() 将 List 类型转换为流类型,咱们还能够经过 list.parallelStream() 转换为并行流。所以你一般可使用 parallelStream 来代替 stream 方法

并行流就是把内容分红多个数据块,使用不一样的线程分别处理每一个数据块的流。这也是流的一大特色,要知道,在 Java 7 以前,并行处理数据集合是很是麻烦的,你得本身去将数据分割开,本身去分配线程,必要时还要确保同步避免竞争。

Stream 让程序员可以比较轻易地实现对数据集合的并行处理,但要注意的是,不是全部状况的适合,有些时候并行甚至比顺序进行效率更低,而有时候由于线程安全问题,还可能致使数据的处理错误,这些我会在下一篇文章中讲解。

比方说下面这个例子

int i = Stream.iterate(1, a -> a + 1).limit(100).parallel().reduce(0, Integer::sum);
复制代码

咱们经过这样一行代码来计算 1 到 100 的全部数的和,咱们使用了 parallel 来实现并行。

但其实是,这样的计算,效率是很是低的,比不使用并行还低!一方面是由于装箱问题,这个前面也提到过,就再也不赘述,还有一方面就是 iterate 方法很难把这些数分红多个独立块来并行执行,所以无形之中下降了效率。

流的可分解性

这就说到流的可分解性问题了,使用并行的时候,咱们要注意流背后的数据结构是否易于分解。好比众所周知的 ArrayList 和 LinkedList,明显前者在分解方面占优。

咱们来看看一些数据源的可分解性状况

数据源可分解性ArrayList极佳LinkedList差IntStream.range极佳Stream.iterate差HashSet好TreeSet好顺序性。

除了可分解性,和刚刚提到的装箱问题,还有一点值得注意的是一些操做自己在并行流上的性能就比顺序流要差,好比:limit,findFirst,由于这两个方法会考虑元素的顺序性,而并行自己就是违背顺序性的,也是由于如此 findAny 通常比 findFirst 的效率要高。

六. 效率

最后再来谈谈效率问题,不少人可能据说过有关 Stream 效率低下的问题。其实,对于一些简单的操做,好比单纯的遍历,查找最值等等,Stream 的性能的确会低于传统的循环或者迭代器实现,甚至会低不少。

可是对于复杂的操做,好比一些复杂的对象归约,Stream 的性能是能够和手动实现的性能匹敌的,在某些状况下使用并行流,效率可能还远超手动实现。好钢用在刀刃上,在适合的场景下使用,才能发挥其最大的用处。

函数式接口的出现主要是为了提升编码开发效率以及加强代码可读性;与此同时,在实际的开发中,并不是老是要求很是高的性能,所以 Stream 与 lambda 的出现意义仍是很是大的。

做者:Howie_Y

主页:www.jianshu.com/u/79638e5f0743


欢迎关注公众号:

公众号微信
相关文章
相关标签/搜索