Stream流管道
1、Stream定义
Stream表示数据流,它没有数据结构,本身也不存储元素,其操作也不会改变源Stream,而是生成新Stream.作为一种操作数据的接口,它提供了过滤、排序、映射、聚合等多种操作方法。
2、Stream有什么用?
Stream当成一个装饰后的Iterator。原始版本的Iterator,用户只能逐个遍历元素并对其执行某些操作;包装后的Stream,用户只要给出需要对其包含的元素执行什么操作,比如“过滤掉长度大于10的字符串”、“获取每个字符串的首字母”等,具体这些操作如何应用到每个元素上,就给Stream就好了!原先是人告诉计算机一步一步怎么做,现在是告诉计算机做什么,计算机自己决定怎么做。
3、如何使用Stream?
Stream可以由数组或集合创建,对流的操作分为两种:
-
中间操作,每次返回一个新的流,可以有多个。
-
终端操作,每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值。
Stream的使用过程有着固定的模式:
创建Stream
通过中间操作,对原始Stream进行“变化”并生成新的Stream
使用终端操作,生成最终结果
4、Stream的特性
Stream有几个特性:
stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。
stream不会改变数据源,通常情况下会产生一个新的集合或一个值。
stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行。
5、一次性的流
流和迭代器类似,只能迭代一次。
Stream<String> stream = list.stream().map(Person::getName).sorted().limit(10);
List<String> newList = stream.collect(Collectors.toList());
List<String> newList2 = stream.collect(Collectors.toList());
//上面代码中第三行会报错,因为第二行已经使用过这个流,这个流已经被消费掉了。
6、Stream的创建
数据源(source)也就是数据的来源,可以通过多种方式获得 Stream 数据源,下面列举几种常见的获取方式。
首先我们先创建一个 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));
Person 类包含年龄和姓名两个成员变量
private String name;
private int age;
6.1、 stream() / parallelStream()
最常用到的方法,将集合转换为流
List list = new ArrayList();
// return Stream<E>
list.stream();
6.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 类型,
6.3、distinct()
去除重复元素,这个方法是通过类的 equals 方法来判断两个元素是否相等的
如例子中的 Person 类,需要先定义好 equals 方法,不然类似[Person{name=‘jack’, age=20}, Person{name=‘jack’, age=20}] 这样的情况是不会处理的。
6.4、sorted() / sorted((T, T) -> int)
如果流中的元素的类实现了 Comparable 接口,即有自己的排序规则,那么可以直接调用 sorted() 方法对元素进行排序,如 Stream
反之, 需要调用 sorted((T, T) -> int) 实现 Comparator 接口
根据年龄大小来比较:
list = list.stream()
.sorted(Comparator.comparingInt(Person::getAge))
.collect(Collectors.toList());
6.5、数字排序
/**
* 数字排序
*/
public static void testIntegerSort() {
List<Integer> list = Arrays.asList(4, 2, 5, 3, 1);
System.out.println(list);//执行结果:[4, 2, 5, 3, 1]
//升序
list.sort((a, b) -> a.compareTo(b.intValue()));
System.out.println(list);//执行结果:[1, 2, 3, 4, 5]
//降序
list.sort((a, b) -> b.compareTo(a.intValue()));
System.out.println(list);//执行结果:[5, 4, 3, 2, 1]
}
6.6、字符串排序
/**
* 字符串排序
*/
public static void testStringSort() {
List<String> list = new ArrayList<>();
list.add("aa");
list.add("cc");
list.add("bb");
list.add("ee");
list.add("dd");
System.out.println(list);//执行结果:aa, cc, bb, ee, dd
//升序
list.sort((a, b) -> a.compareTo(b.toString()));
System.out.println(list);//执行结果:[aa, bb, cc, dd, ee]
//降序
list.sort((a, b) -> b.compareTo(a.toString()));
System.out.println(list);//执行结果:[ee, dd, cc, bb, aa]
}
6.7、对象字段排序
/**
* 对象串排序
*/
public void testObjectSort() {
List<Person> list = new ArrayList<>();
list.add(new Person("三炮", 48));
list.add(new Person("老王", 35));
list.add(new Person("小明", 8));
list.add(new Person("叫兽", 70));
System.out.println(list); //执行结果:[Person{name='三炮', age=48}, Person{name='老王', age=35}, Person{name='小明', age=8}, Person{name='叫兽', age=70}]
//按年龄升序
list.sort((a, b) -> Integer.compare(a.age, b.getAge()));
System.out.println(list);//执行结果:[Person{name='小明', age=8}, Person{name='老王', age=35}, Person{name='三炮', age=48}, Person{name='叫兽', age=70}]
//按年龄降序
list.sort((a, b) -> Integer.compare(b.age, a.getAge()));
System.out.println(list);//执行结果:[Person{name='叫兽', age=70}, Person{name='三炮', age=48}, Person{name='老王', age=35}, Person{name='小明', age=8}]
//如果按姓名排序,其实就是按字符串排序一样
}
6.8、limit(long n)
返回前 n 个元素
list = list.stream()
.limit(2)
.collect(Collectors.toList());
//打印输出 [Person{name='jack', age=20}, Person{name='mike', age=25}]
6.9、skip(long n)
去除前 n 个元素
list = list.stream()
.skip(2)
.collect(Collectors.toList());
//打印输出 [Person{name='tom', age=30}]
注意:skip(m)用在 limit(n) 前面时,先去除前 m 个元素再返回剩余元素的前 n 个元素
limit(n) 用在 skip(m) 前面时,先返回前 n 个元素再在剩余的 n 个元素中去除 m 个元素
7、map(T -> R) 将流中的每一个元素 T 映射为 R(类似类型转换)
List<String> newlist = list.stream().map(Person::getName).collect(Collectors.toList());
7.1、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(toList());
上面例子中,我们的目的是把 List 中每个字符串元素以” “分割开,变成一个新的 List。
首先 map 方法分割每个字符串元素,但此时流的类型为 Stream。
7.2、anyMatch(T -> boolean)
流中是否有一个元素匹配给定的 T -> boolean 条件
是否存在一个 person 对象的 age 等于 20:
boolean b = list.stream().anyMatch(person -> person.getAge() == 20);
7.3、 allMatch(T -> boolean)
流中是否所有元素都匹配给定的 T -> boolean 条件
boolean result = list.stream().allMatch(Person::isStudent);
7.4、findAny() 和 findFirst()
findAny():找到其中一个元素 (使用 stream() 时找到的是第一个元素;使用 parallelStream()并行时找到的是其中一个元素)
findFirst():找到第一个元素
值得注意的是,这两个方法返回的是一个 Optional 对象,它是一个容器类,能代表一个值存在或不存在
7.5、count() 返回流中元素个数,结果为 long 类型。
7.6、collect() 收集方法,我们很常用的是 collect(toList()),当然还有 collect(toSet()) 等,参数是一个收集器接口
7.7、forEach()
//打印各个元素:
list.stream().forEach(System.out::println);
再比如说 MyBatis 里面访问数据库的 mapper 方法:
//向数据库插入新元素:
list.stream().forEach(PersonMapper::insertPerson);
数值流
- mapToInt(T -> int) : return IntStream
- mapToDouble(T -> double) : return DoubleStream
- mapToLong(T -> long) : return LongStream
IntStream intStream = list.stream().mapToInt(Person::getAge);
函数汇总
1、counting
long l = list.stream().count();
2、summingInt ,summingLong ,summingDouble
summing,没错,也是计算总和,不过这里需要一个函数参数
计算 Person 年龄总和:
int sum = list.stream().mapToInt(Person::getAge).sum();
3、averagingInt,averagingLong,averagingDouble
求平均数
OptionalDouble average = list.stream().mapToInt(Person::getAge).average();
4、summarizingInt,summarizingLong,summarizingDouble
IntSummaryStatistics l = list.stream().collect(summarizingInt(Person::getAge));
IntSummaryStatistics 包含了计算出来的平均值,总数,总和,最值,可以通过下面这些方法获得相应的数据
5、取最值
maxBy,minBy 两个方法,需要一个 Comparator 接口作为参数
Optional<Person> optional = list.stream().collect(maxBy(comparing(Person::getAge)));
//
Optional<Person> optional = list.stream().max(comparing(Person::getAge));
6、joining 连接字符串
也是一个比较常用的方法,对流里面的字符串元素进行连接,其底层实现用的是专门用于字符串连接的 StringBuilder
String s = list.stream().map(Person::getName).collect(joining());
//结果:jackmiketom
String s = list.stream().map(Person::getName).collect(joining(","));
//结果:jack,mike,tom
//joining 还有一个比较特别的重载方法:即 Today 放开头,play games. 放结尾,and 在中间连接各个字符串
String s = list.stream().map(Person::getName).collect(joining(" and ", "Today ", " play games."));
//结果:Today jack and mike and tom play games.
7、groupingBy 分组
groupingBy 用于将数据分组,最终返回一个 Map 类型
Map<Integer, List<Person>> map = list.stream().collect(groupingBy(Person::getAge));
//例子中我们按照年龄 age 分组,每一个 Person 对象中年龄相同的归为一组。
Map<String,List<Person>> result = list.stream()
.collect(Collectors.groupingby((person)->{
if(person.getAge()>60)
return "老年人";
else if(person.getAge()>40)
return "中年人";
else
return "青年人";
}));
//另外可以看出,Person::getAge 决定 Map 的键(Integer 类型),list 类型决定 Map 的值(List 类型)
并行
我们通过 list.stream() 将 List 类型转换为流类型,我们还可以通过 list.parallelStream() 转换为并行流。
并行流就是把内容分成多个数据块,使用不同的线程分别处理每个数据块的流。这也是流的一大特点,要知道,在 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 的效率要高。
如众所周知的 ArrayList 和 LinkedList,明显前者在分解方面占优。
我们来看看一些数据源的可分解性情况
数据源 | 可分解性 |
---|---|
ArrayList | 极佳 |
LinkedList | 差 |
IntStream.range | 极佳 |
Stream.iterate | 差 |
HashSet | 好 |
TreeSet | 好 |
顺序性
除了可分解性,和刚刚提到的装箱问题,还有一点值得注意的是一些操作本身在并行流上的性能就比顺序流要差,比如:limit,findFirst,因为这两个方法会考虑元素的顺序性,而并行本身就是违背顺序性的,也是因为如此 findAny 一般比 findFirst 的效率要高。