一. 简介
Stream(流)是Java 8 提供的高效操作集合类(Collection)数据的API。
优点:
- 借助 Lambda 表达式,提高效率和可读性。
- 集成各种操作,极大的提高效率和代码维护。
- 提供串行和并行处理。
- 无需担心时间复杂度问题,因为流是先把所有操作集合到一起,然后再对数据遍历处理。
二. 相关用法
使用Stream 的基本步骤,
- 创建Stream
- 转换Stream,每次转换原有Stream对象不改变,返回一个新的Stream对象(可以有多次转换)
- 对Stream 进行聚合(Reduce)操作,获取想要的结果
1. 创建Stream
创建Stream 有两种方式,一种是通过Stream接口的静态工厂方法(Java8里接口可以带静态方法);另一种是通过Collection接口的默认方法(默认方法:Default method,也是Java8中的一个新特性,就是接口中的一个带有实现的方法)–stream(),把一个Collection对象转换成Stream。
1)使用静态方法创建Stream
// 1\. 通过of 方法
Stream stream = Stream.of(1, 2, 3, 4, 5);
// 2\. 通过generate 方法创建无穷stream
Stream stream1 = Stream.generate(new Supplier() {
@Override
public Double get() {
return Math.random();
}
});
// 上面的可以写成lambda 表达式
Stream stream1 = Stream.generate(() -> Math.random());
// 或方法引用
Stream stream1 = Stream.generate(Math::random);
上例通过generate 方法是生成一个无限长度的Stream,其中值是随机的。这个无限长度Stream 是懒加载的,一般这种无限长度的Stream都会配合Stream的limit()方法来用。
iterate方法:也是生成无限长度的Stream,和generator不同的是,其元素的生成是重复对给定的种子值(seed)调用用户指定函数来生成的。其中包含的元素可以认为是:seed,f(seed),f(f(seed)) 无限循环,例:
Stream.iterate(1, it -> it + 2).limit(6).forEach(System.out::println);
打印一个从1 开始的公差为2 的等差数列的前6 项,注意,如果不调用limit 方法将一直打印下去。
2)调用Collection 的子类的stream() 方法
List strings = Arrays.asList("a", "ab", "abc", "abcd", "abcde");
Stream stream = strings.stream();
2. Stream 的操作
1)操作类型
Intermediate:中间操作,一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
包括:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unorderedTerminal:终止操作,一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。
包括:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iteratorShort-circuiting: 对于一个 intermediate 操作,如果它接受的是一个无限大(infinite/unbounded)的 Stream,但返回一个有限的新 Stream。 对于一个 terminal 操作,如果它接受的是一个无限大的 Stream,但能在有限的时间计算出结果。 当操作一个无限大的 Stream,而又希望在有限时间内完成操作,则在管道内拥有一个 short-circuiting 操作是必要非充分条件。
包括:anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
2)转换流的操作
distinct : 对于Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素;
List strings = Arrays.asList("a", "ab", "ab", "abc", "abcd", "abcde");
strings.stream().distinct().forEach(System.out::println);
上例代码会将重复的“ab” 去除。
filter :对于Stream中包含的元素使用给定的过滤函数进行过滤操作,新生成的Stream只包含符合条件的元素;
List strings = Arrays.asList("a", "ab", "ab", "abc", "abcd", "abcde");
// 筛选包含“c” 的字符串
strings.stream().filter(it -> it.contains("c")).forEach(System.out::println);
map : 对于Stream中包含的元素使用给定的转换函数进行转换操作,新生成的Stream只包含转换生成的元素。这个方法有三个对于原始类型的变种方法,分别是:mapToInt,mapToLong和mapToDouble。这三个方法也比较好理解,比如mapToInt就是把原始Stream转换成一个新的Stream,这个新生成的Stream中的元素都是int类型。之所以会有这样三个变种方法,可以免除自动装箱/拆箱的额外消耗;
List strings = Arrays.asList("a", "ab", "ab", "abc", "abcd", "abcde");
// 将字符串中的字母 “a” 替换成 “x”
strings.stream().map(it -> it.replace('a', 'x')).forEach(System.out::println);
flatMap :map 生成的是个 1:1 映射,每个输入元素,都按照规则转换成为另外一个元素。还有一些场景,是一对多映射关系的,这时需要 flatMap。
List> lists = Arrays.asList(
Arrays.asList("ab", "abc", "abcd"),
Arrays.asList("xy", "xyz"),
Arrays.asList("6")
);
// 输出 三个Stream 对象
lists.stream().map(it -> it.stream().filter(s -> !s.equals("6"))).forEach(System.out::println);
// 输出 字符串
lists.stream().flatMap(str -> str.stream().filter(s -> !s.equals("6"))).forEach(System.out::println);
flatMap 把 输入 Stream 中的层级结构扁平化,就是将最底层元素抽出来放到一起,最终 输出 的新 Stream 里面已经没有 List 了,都是直接的字符串。
peek :生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数,可用于调试,在转换流的过程中输出流中元素的值,如:
List ss = strings.stream().filter(str -> str.length() > 2).peek(System.out::println)
.map(str -> str.replace('a', 'x'))
.peek(System.out::println)
.collect(Collectors.toList());
上例可以观察在map 操作前后每个元素的输出。
limit :对一个Stream进行截断操作,获取其前N个元素,如果原Stream中包含的元素个数小于N,那就获取其所有的元素
List strings = Arrays.asList("a", "ab", "ab", "abc", "abcd", "abcde");
// 输出前 3 个元素
strings.stream().limit(3).forEach(System.out::println);
skip :返回一个丢弃原Stream的前N个元素后剩下元素组成的新Stream,如果原Stream中包含的元素个数小于N,那么返回空Stream
List strings = Arrays.asList("a", "ab", "ab", "abc", "abcd", "abcde");
// 跳过前 3 个元素,输出后面的元素
strings.stream().skip(3).forEach(System.out::println);
sorted :对流内的元素进行排序,sorted 方法有两种重载,第一种是无参,此时要求元素需要实现Comparable 接口,第二种是传一个Comparator 类型参数
static class P {
int age;
String name;
P(String name, int age) {
this.name = name;
this.age = age;
}
}
List ps = Arrays.asList(new P("abc", 12), new P("xyz", 23),
new P("bcd", 666), new P("opq", 33), new P("abc", 12));
// 传入一个lambda 表达式作为排序条件
ps.stream().distinct().sorted((o1, o2) -> o1.age - o2.age).forEach(System.out::println);
3)对流取结果(reduction)
这些操作都是terminal 操作,下面介绍一些比较常用的。
collect :它可以把Stream中的要有元素收集到一个结果容器中(比如Collection)。先看一下最通用的collect方法的定义:
R collect(Supplier supplier,
BiConsumer accumulator,
BiConsumer combiner);
先来看看这三个参数的含义:
Supplier supplier 是一个工厂函数,用来生成一个新的容器;
BiConsumer accumulator 也是一个函数,用来把Stream中的元素添加到结果容器中;
BiConsumer combiner 还是一个函数,用来把中间状态的多个结果容器合并成为一个(并发的时候会用到)。示例
List nums = Lists.newArrayList(1,1,null,2,3,4,null,5,6,7,8,9,10);
List numsWithoutNull = nums.stream().filter(num -> num != null).
collect(() -> new ArrayList(), // 创建新的List 实例
(list, item) -> list.add(item), // 将item 添加到list 中
(list1, list2) -> list1.addAll(list2)); // 把第二个list 全部加入第一个中,用于并发
上面代码就是将一个list 过滤掉null 然后收集到一个新的容器中。
上例看起来代码很多,很复杂,collect 方法还有更加简单的使用方式,定义如下
R collect(Collector super T, A, R> collector);
同时,Java8还给我们提供了Collector的工具类 – Collectors,其中已经定义了一些静态工厂方法,比如:Collectors.toCollection() 收集到Collection中, Collectors.toList() 收集到List 中和Collectors.toSet() 收集到Set中,等等。所以上例可简化成:
List numsWithoutNull = nums.stream().filter(num -> num != null).
collect(Collectors.toList());
Collectors.toArray() —— 由stream中的元素得到的数组,默认是Object[],可以通过参数设置需要结果的类型:
String[] words2 = Stream.of("You", "may", "assume").toArray(String[]::new);
Collectors.toMap() —— 将stream中的元素映射为 map 的形式,两个参数分别用于生成对应的key和value的值:
List strings = Arrays.asList("abc", "ab", "abcde", "a", "abcd", "ab");
// 把字符串中 a 替换为 x 作为key,在字符串后加一个 “-233” 作为value
Map map = strings.stream().collect(Collectors.toMap(str -> str.replace('a', 'x'), str -> str + "-233", (oldStr, newStr) -> newStr));
如果一个key对应多个value,则会抛出异常,需要使用第三个参数设置如何处理冲突,比如仅使用原来的value、使用新的value,或者合并。
reduce :这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce。
reduce 有两种常用的重载方法,第一个是接收一个BinaryOperator 类型参数,BinaryOperator 参数是一个执行双目运算的 Functional Interface ,假如这个参数表示的操作为op,stream中的元素为x, y, z, …,则 reduce() 执行的就是 x op y op z ...,所以要求op这个操作具有结合性(associative),即满足: (x op y) op z = x op (y op z)
Optional reduce(BinaryOperator accumulator);
这个方法返回一个Optional 对象,这是Java 8 中一个防止NPE 的类型,之后再介绍。示例:
List strings = Arrays.asList("abc", "ab", "abcde", "a", "abcd", "ab");
Optional op = strings.stream().distinct().filter(str -> str.length() > 1).sorted().reduce((a, b) -> a + " - " + b);
op.ifPresent(System.out::println);
// 输出 ab - abc - abcd - abcde
第二个方法接收一个初始值 和 BinaryOperator 参数:
T reduce(T identity, BinaryOperator accumulator);
和上面方法不同的是,它允许用户提供一个循环计算的初始值,如果Stream为空,就直接返回该值。而且这个方法不会返回Optional,因为其不会出现null值。
String ss = strings.stream().distinct().filter(str -> str.length() > 1).sorted().reduce("hhh", (a, b) -> a + " - " + b);
System.out.println(ss);
// 输出 hhh - ab - abc - abcd - abcde
其他方法:
count :获取Stream中元素的个数。
allMatch:是不是Stream中的所有元素都满足给定的匹配条件,返回boolean
anyMatch:Stream中是否存在任何一个元素满足匹配条件,返回boolean
findFirst : 返回Stream中的第一个元素,如果Stream为空,返回空Optional
noneMatch:是不是Stream中的所有元素都不满足给定的匹配条件
max 和 min:使用给定的比较器(Operator),返回Stream中的最大|最小值 的Optional 对象,例
List strings = Arrays.asList("abc", "ab", "abcde", "a", "abcd", "ab");
strings.stream().max(Comparator.comparing(String::length)).ifPresent(System.out::println);
// 输出 abcde
4)分组(Grouping 和 Partitioning)
groupingBy:表示根据某一个字段或条件进行分组,返回一个Map,其中key为分组的字段或条件,value默认为list, groupingByConcurrent() 是其并发版本
List strings = Arrays.asList("abc", "ab", "abcde", "a", "abcd", "ab");
// 根据字符串长度分组
Map> map = strings.stream().collect(Collectors.groupingBy(String::length));
如果 groupingBy() 分组的依据是一个bool条件,则key的值为true/false,此时与 partitioningBy() 等价,但是 partitioningBy() 的效率更高:
List strings = Arrays.asList("abc", "ab", "abcde", "a", "abcd", "ab");
// 根据字符串长度是否是偶数,分成两部分
Map> map = strings.stream().collect(Collectors.partitioningBy(str -> str.length() % 2 == 0));
groupingBy() 提供第二个参数,表示 downstream ,即对分组后的value作进一步的处理,可以有如下操作
// 分组结果返回为 Set 而不是List
Map> map = strings.stream().collect(Collectors.groupingBy(String::length, Collectors.toSet()));
// 返回value 集合中元素个数
Map map = strings.stream().collect(Collectors.groupingBy(String::length, Collectors.counting()));
// 对value 集合中元素的某个值求和,同理还有summingDouble,summingLong
Map map = strings.stream().collect(Collectors.groupingBy(String::length, Collectors.summingInt(String::length)));
// 对value 集合中元素求最大值,注意求出的最大值是Optional 类型
Map> map = strings.stream().collect(Collectors.groupingBy(String::length, Collectors.maxBy(Comparator.comparing(String::length))));
// 使用mapping对value的字段进行map处理
Map> map = strings.stream().collect(Collectors.groupingBy(String::length, Collectors.mapping(str -> str.replace('a', 'x'), Collectors.toSet())));
// 通过 summarizing[Int|Double|Long] 获取统计结果, 返回一个value 为 [Int|Double|Long]SummaryStatistics 的实例,可以调用该实例的getAverage 等方法获得统计值
Map map = strings.stream().collect(Collectors.groupingBy(String::length, Collectors.summarizingInt(String::length)));
// 通过reduceing 可以做更多处理,但是并不常用
5)并行操作
Stream 支持并发操作,前提是需要满足以下几点
构造一个paralle stream,默认构造的 stream 是顺序执行的,调用 paralle() 构造并行的stream
要执行的操作必须是可并行执行的,即并行执行的结果和顺序执行的结果是一致的,而且必须保证 stream 中执行的操作是线程安全的
List sss = strings.stream().parallel().distinct().collect(Collectors.toList());
常见使用场景:
使stream无序,对于 distinct() 和 limit() 等方法,如果不关心顺序,则可以使用并行;
在 groupingBy() 的操作中,map的合并操作是比较重的,可以通过 groupingByConcurrent() 来并行处理,不过前提是parallel stream;
注意:在执行stream操作时不能修改stream对应的collection。