JDK1.8系列文章
- JDK1.8新特性(一):Lambda表达式
- JDK1.8新特性(二):Optional 类
- JDK1.8新特性(三):Stream
- JDK1.8新特性(四):Maps
- JDK1.8新特性(五):新的日期时间 API
Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。Stream API 提供了一种高效且易于使用的处理数据的方式。
一、什么是 Stream
Stream 中文称为 “流”,通过将集合转换为这么一种叫做 “流” 的元素序列,通过声明性方式,能够对集合中的每个元素进行一系列并行或串行的流水线操作。
换句话说,你只需要告诉流你的要求,流便会在背后自行根据要求对元素进行处理,而你只需要 “坐享其成”。
二、流操作
整个流操作就是一条流水线,将元素放在流水线上一个个地进行处理。
其中数据源便是原始集合,然后将如 List 的集合转换为 Stream 类型的流,并对流进行一系列的中间操作,比如过滤保留部分元素、对元素进行排序、类型转换等;最后再进行一个终端操作,可以把 Stream 转换回集合类型,也可以直接对其中的各个元素进行处理,比如打印、比如计算总数、计算最大值等等
很重要的一点是,很多流操作本身就会返回一个流,所以多个操作可以直接连接起来,我们来看看一条 Stream 操作的代码:
List<String> list = new ArrayList<>();
// 在list中增加20个字符串元素
list.add("Lucas");
List<Integer> streamList = list.stream().map(String::length).sorted().limit(10).collect(Collectors.toList());
// stream() 将集合转换为流
// map(String::length) 将原来的 List<String> 转换为 List<Integer>
// sorted() 排序
// limit(10) 保留前10个元素
// collect(Collectors.toList()) 将流转换回集合
如果是以前,进行这么一系列操作,你需要做个迭代器或者 foreach 循环,然后遍历,一步步地亲力亲为地去完成这些操作;但是如果使用流,你便可以直接声明式地下指令,流会帮你完成这些操作。
三、流与集合的差异
1、何时进行计算
一个集合,它会包含当前数据结构中所有的值,你可以随时增删,但是集合里面的元素毫无疑问地都是已经计算好了的。
流则是按需计算,按照使用者的需要计算数据,你可以想象我们通过搜索引擎进行搜索,搜索出来的条目并不是全部呈现出来的,而且先显示最符合的前 10 条或者前 20 条,只有在点击 “下一页” 的时候,才会再输出新的 10 条。再比方在线观看电影和你硬盘里面的电影,也是差不多的道理。
2、迭代方式
Stream的迭代方式是内部迭代,集合的迭代方式外部迭代。
我们可以把集合比作一个工厂的仓库,一开始工厂比较落后,要对货物作什么修改,只能工人亲自走进仓库对货物进行处理,有时候还要将处理后的货物放到一个新的仓库里面。在这个时期,我们需要亲自去做迭代,一个个地找到需要的货物,并进行处理,这叫做外部迭代。
后来工厂发展了起来,配备了流水线作业,只要根据需求设计出相应的流水线,然后工人只要把货物放到流水线上,就可以等着接收成果了,而且流水线还可以根据要求直接把货物输送到相应的仓库。这就叫做内部迭代,流水线已经帮你把迭代给完成了,你只需要说要干什么就可以了(即设计出合理的流水线)。
流和迭代器类似,只能迭代一次。下面代码中第三行会报错,因为第二行已经使用过这个流,这个流已经被消费掉了。
List<String> list = new ArrayList<>();
Stream<Integer> stream =list.stream().map(String::length).sorted().limit(10);
List<Integer> newList = stream.collect(Collectors.toList());
List<Integer> newList2 = stream.collect(Collectors.toList()); // 报错
Java 8 引入 Stream 很大程度是因为,流的内部迭代可以自动选择一种合适你硬件的数据表示和并行实现;而以往程序员自己进行 foreach 之类的时候,则需要自己去管理并行等问题。
四、常用方法
首先我们先创建一个 Person 测试类,包含年龄的姓名两个变量
public class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
再创建一个 Person 泛型的List
List<Person> list = new ArrayList<>();
list.add(new Person("jack", 20));
list.add(new Person("mike", 25));
list.add(new Person("tom", 30));
1、stream() / parallelStream()
最常用到的方法,将集合转换为流
Stream stream = list.stream();
而 parallelStream()
是并行流方法,能够让数据集执行并行操作,后面会更详细地讲解
2、filter(T -> boolean)
保留 boolean 为 true 的元素
// 保留年龄为 20 的 person 元素
list = list.stream()
.filter(person -> person.getAge() == 20)
.collect(Collectors.toList());
打印输出 [Person{name='jack', age=20}],collect(toList()) 可以把流转换为 List 类型,这个以后会讲解
3、distinct()
去除重复元素,这个方法是通过类的 equals 方法来判断两个元素是否相等的
如例子中的 Person 类,需要先定义好 equals 方法,不然类似 [Person{name='jack', age=20}, Person{name='jack', age=20}]
这样的情况是不会处理的
4、sorted() / sorted((T, T) -> int)
如果流中的元素的类实现了 Comparable 接口,即有自己的排序规则,那么可以直接调用 sorted() 方法对元素进行排序,如 Stream
反之, 需要调用 sorted((T, T) -> int)
实现 Comparator 接口
// 根据年龄大小来比较:
list = list.stream()
.sorted((p1, p2) -> p1.getAge() - p2.getAge())
.collect(Collectors.toList());
当然这个可以简化为
list = list.stream()
.sorted(Comparator.comparingInt(Person::getAge))
.collect(Collectors.toList());
5、limit(long n)
返回前 n 个元素
list = list.stream()
.limit(2)
.collect(Collectors.toList());
// 打印输出 [Person{name='jack', age=20}, Person{name='mike', age=25}]
6、skip(long n)
去除前 n 个元素
list = list.stream()
.skip(2)
.collect(Collectors.toList());
// 打印输出 [Person{name='tom', age=30}]
tips:
- 用在 limit(n) 前面时,先去除前 m 个元素再返回剩余元素的前 n 个元素
- limit(n) 用在 skip(m) 前面时,先返回前 n 个元素再在剩余的 n 个元素中去除 m 个元素
list = list.stream()
.limit(2)
.skip(1)
.collect(Collectors.toList());
// 打印输出 [Person{name='mike', age=25}]
7、map(T -> R)
将流中的每一个元素 T 映射为 R(类似类型转换)
List<String> newlist = list.stream()
.map(Person::getName)
.collect(Collectors.toList());
// 打印输出 [jack, mike, tom]
newlist 里面的元素为 list 中每一个 Person 对象的 name 变量
8、flatMap(T -> Stream)
将流中的每一个元素 T 映射为一个流,再把每一个流连接成为一个流
List<String> list = new ArrayList<>();
list.add("aaa bbb ccc");
list.add("ddd eee fff");
list.add("ggg hhh iii");
list = list.stream()
.map(s -> s.split(" "))
.flatMap(Arrays::stream)
.collect(Collectors.toList());
// 打印输出 [aaa, bbb, ccc, ddd, eee, fff, ggg, hhh, iii]
上面例子中,我们的目的是把 List 中每个字符串元素以" "分割开,变成一个新的 List。首先 map 方法分割每个字符串元素,但此时流的类型为 Stream<String[ ]>,因为 split 方法返回的是 String[ ] 类型;所以我们需要使用 flatMap 方法,先使用Arrays::stream将每个 String[ ] 元素变成一个 Stream 流,然后 flatMap 会将每一个流连接成为一个流,最终返回我们需要的 Stream
9、anyMatch(T -> boolean)
流中是否有一个元素匹配给定的 T -> boolean
条件
// 是否存在一个 person 对象的 age 等于 20:
boolean b = list.stream().anyMatch(person -> person.getAge() == 20);
10、allMatch(T -> boolean)
流中是否所有元素都匹配给定的 T -> boolean
条件
11、noneMatch(T -> boolean)
流中是否没有元素匹配给定的 T -> boolean
条件
12、findAny() 和 findFirst()
- findAny():找到其中一个元素 (使用 stream() 时找到的是第一个元素;使用 parallelStream() 并行时找到的是其中一个元素)
- findFirst():找到第一个元素
值得注意的是,这两个方法返回的是一个 Optional 对象,它是一个容器类,代表一个值存在或不存在
13、reduce((T, T) -> T) 和 reduce(T, (T, T) -> T)
用于组合流中的元素,如求和,求积,求最大值等
// 计算年龄总和:
int sum = list.stream().map(Person::getAge).reduce(0, (a, b) -> a + b);
// 与之相同:
int sum = list.stream().map(Person::getAge).reduce(0, Integer::sum);
其中,reduce 第一个参数 0 代表起始值为 0,lambda (a, b) -> a + b 即将两值相加产生一个新值,同样地:
// 计算年龄总乘积:
int sum = list.stream().map(Person::getAge).reduce(1, (a, b) -> a * b);
当然也可以
Optional<Integer> sum = list.stream().map(Person::getAge).reduce(Integer::sum);
即不接受任何起始值,但因为没有初始值,需要考虑结果可能不存在的情况,因此返回的是 Optional 类型
14、count()
返回流中元素个数,结果为 long 类型
15、collect()
收集方法,我们很常用的是 collect(toList())
,当然还有 collect(toSet())
等,参数是一个收集器接口
16、forEach()
对元素进行迭代
// 打印各个元素:
list.stream().forEach(System.out::println);
五、数据流
前面介绍的如 int sum = list.stream().map(Person::getAge).reduce(0, Integer::sum);
计算元素总和的方法其中暗含了装箱成本,map(Person::getAge)
方法过后流变成了 Stream 类型,而每个 Integer 都要拆箱成一个原始类型再进行 sum 方法求和,这样大大影响了效率。
针对这个问题 Java 8 引入了数值流 IntStream, DoubleStream, LongStream,这种流中的元素都是原始数据类型,分别是 int,double,long
1、流与数值流的转换
1.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)
1.2、数值流转换为流
很简单,就一个 boxed
Stream<Integer> stream = intStream.boxed();
2、数值流方法
下面这些方法作用不用多说,看名字就知道:
- sum()
- max()
- min()
- average() 等...
3、数值范围
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();
六、构建流
之前我们得到一个流是通过一个原始数据源转换而来,其实我们还可以直接构建得到流。
1、值创建流
Stream.of(T...) : Stream.of("aa", "bb") 生成流
// 生成一个字符串流
Stream<String> stream = Stream.of("aaa", "bbb", "ccc");
Stream.empty() : 生成空流
2、数组创建流
根据参数的数组类型创建对应的流:
- 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);
3、文件生成流
Stream<String> stream = Files.lines(Paths.get("data.txt"));
每个元素是给定文件的其中一行
4、函数生成流
两个方法:
- 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(Collectors.toList());
// 如果 Map 的 Key 重复了,可是会报错的哦
Map<Integer, Person> map = list.stream().collect(Collectors.toMap(Person::getAge, p -> p));
2、汇总
- 计算总数 count()
- 计算总和 sum()
- 求平均数 average()
// 计算总数
long size = list.stream().count();
// 计算总和
int sum = list.stream().mapToInt(Person::getAge).sum();
// 求平均数
Double average = list.stream().collect(Collectors.averagingInt(Person::getAge));
- summarizingInt,summarizingLong,summarizingDouble这三个方法比较特殊,比如 summarizingInt 会返回 IntSummaryStatistics 类型
IntSummaryStatistics l = list.stream().collect(Collectors.summarizingInt(Person::getAge));
IntSummaryStatistics 包含了计算出来的平均值,总数,总和,最值,可以通过下面这些方法获得相应的数据,getAverage()、getCount()、getMax()、getMin()、getSum()。
3、取最值
maxBy,minBy 两个方法,需要一个 Comparator 接口作为参数
Optional<Person> optional = list.stream().max(Comparator.comparing(Person::getAge));
4、joining 连接字符串
也是一个比较常用的方法,对流里面的字符串元素进行连接,其底层实现用的是专门用于字符串连接的 StringBuilder
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 在中间连接各个字符串
5、groupingBy 分组
groupingBy 用于将数据分组,最终返回一个 Map 类型
Map<Integer, List<Person>> map = list.stream().collect(Collectors.groupingBy(Person::getAge));
例子中我们按照年龄 age 分组,每一个 Person 对象中年龄相同的归为一组
另外可以看出,Person::getAge
决定 Map 的键(Integer 类型),list 类型决定 Map 的值(List 类型)
(一)多级分组groupingBy 可以接受一个第二参数实现多级分组:
Map<Integer, Map< String , List<Person>>> map = list.stream().collect(Collectors.groupingBy(Person::getAge, Collectors.groupingBy(Person::getName)));
其中返回的 Map 键为 Integer 类型,值为 Map<String, List> 类型,即参数中 groupBy(...) 返回的类型
(二)按组收集数据
Map<Integer, Integer> map = list.stream().collect(Collectors.groupingBy(Person::getAge, Collectors.summingInt(Person::getAge)));
该例子中,我们通过年龄进行分组,然后 summingInt(Person::getAge))
分别计算每一组的年龄总和(Integer),最终返回一个 Map<Integer, Integer>
6、partitioningBy 分区
分区与分组的区别在于,分区是按照 true 和 false 来分的,因此partitioningBy 接受的参数的 lambda 也是 T -> boolean
// 根据年龄是否小于等于20来分区
Map<Boolean, List<Person>> map = list.stream()
.collect(Collectors.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 | 好 |
九、效率
最后再来谈谈效率问题,很多人可能听说过有关Stream 效率底下的问题。其实,对于一些简单的操作,比如单纯的遍历,查找最值等等,Stream 的性能的确会低于传统的循环或者迭代器实现,甚至会低很多。
但是对于复杂的操作,比如一些复杂的对象归约,Stream 的性能是可以和手动实现的性能匹敌的,在某些情况下使用并行流,效率可能还远超手动实现。好钢用在刀刃上,在适合的场景下使用,才能发挥其最大的用处。
函数式接口的出现主要是为了提高编码开发效率以及增强代码可读性;与此同时,在实际的开发中,并非总是要求非常高的性能,因此 Stream 与 lambda 的出现意义还是非常大的。
十、公众号
如果大家想要第一时间看到我更新的 Java 方向学习文章,可以关注一下公众号【Lucas的咖啡店】。所有学习文章公众号首发,请各位大大扫码关注一下哦!
Java 8 新特性学习视频请关注公众号,私信【Java8】即可免费无套路获取学习视频。