掌握了Lambda表达式和方法引用之后,我们就可以很轻松的 Stream API
了,没有掌握Lambda表达式和方法引用的话,建议去看看我之前写的文章。因为接下来会大量的用到它们。
Stream
什么是Stream
?
Stream
是一组用来处理数组、集合的API,它支持顺序流和并行流的聚合操作。
Stream特性
- 不是数据结构,没有内部存储。
- 不支持索引访问。
- 延迟计算。
- 支持并行。
- 很容易生成数组或集合(
List
、Set
)。 - 支持过滤、查找、转换、汇总、聚合等操作。
备注:
1、在没有执行终止操作之前,它是不会进行计算的。
Stream运行机制
Stream
分为数据源,中间操作,终止操作。
数据源可以是一个数组、一个集合、一个生成器方法、一个I/O通道等等。
一个流可以有零个或者多个中间操作,每一个中间操作都会返回一个新的流,供下一个操作使用。一个流只会有一个终止操作。
Stream
只有遇到终止操作,它的数据源才开始执行遍历操作。
Stream常用的API
中间操作
- 过滤
filter
- 去重
distinct
- 排序
sorted
- 截取
limit
、skip
- 转换
map
、flatMap
- 其他
peek
终止操作
- 循环
forEach
- 计算
min
、max
、count
、average
- 匹配
anyMatch
、allMatch
、noneMatch
、findFirst
、findAny
- 汇聚
reduce
- 收集器
toArray
、collect
Stream的创建
- 通过数组来创建。
- 通过集合来创建。
- 通过
Stream.generate
方法来创建。 - 通过
Stream.iterate
方法来创建。 - 通过其他API来创建(文件、字符串等等)。
这里说明一下,Stream API
在Android使用的话,要求API 24 以上才能使用,也可以导入com.annimon:stream:$lastVersion
库来使用它。
了解了一下基本的概念后,下面我们来看看Stream API
实战。
Stream实战
首先我们看看用代码如何来实现Stream
的创建。
1、通过数组来创建。
public static<T> Stream<T> of(T... values) {
...
}
String[] arr = {"a", "b", "c", "1", "2"};
Stream<String> stream = Stream.of(arr);
使用Stream.of
方法将数组作为参数传入,即可得到 Stream
。
2、通过集合来创建。
default Stream<E> stream() {
...
}
List<String> list = Arrays.asList("a", "b", "c", "1", "2");
Stream<String> stream = list.stream();
使用list.stream
方法创建得到Stream
。
3、通过Stream.generate
方法来创建。
public static<T> Stream<T> generate(Supplier<T> s) {
...
}
Stream<String> stream = Stream.generate(() -> 1);
使用Stream.generate
方法创建得到Stream
时,传入的是Supplier
,它代表一个输出,说明调用Stream.generate
方法会不断的输入一个值,放到Stream中。对于Stream.generate
方法的使用,一般都会和一些中间操作的API搭配使用,比如使用 limit
截取一部分数据。
4、通过Stream.iterate
方法来创建。
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) {
...
}
Stream<Integer> stream = Stream.iterate(1, x -> x + 1);
使用Stream.iterate
方法创建得到Stream
时,传入的是一个初始值和一个一元运算(输入和输出类型相同)的Lambda表达式。上面的操作解释为输入一个初始值为1,每次的输入结果为上一次输出的值加1,然后不然循环输入。
备注:Stream.generate
和 Stream.iterate
方法生成的是一个无止尽的 Stream
,我们在使用的时候需要注意这一点。
5、通过其他API来创建。
String str = "abcd";
IntStream stream = str.chars();
Stream<String> stream = Files.lines(Paths.get("path"));
我们也可以使用一些其他API自带的创建 Stream
的方法。
接下来看看 Stream
常用的API的使用。
1、中间操作符。
Stream<T> filter(Predicate<? super T> predicate);
Stream<T> distinct();
Stream<T> sorted();
Stream<T> limit(long maxSize);
Stream<T> skip(long n);
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
Stream<T> peek(Consumer<? super T> action);
S parallel();
S sequential();
2、终止操作符。
void forEach(Consumer<? super T> action);
Optional<T> min(Comparator<? super T> comparator);
Optional<T> max(Comparator<? super T> comparator);
long count();
OptionalDouble average();
boolean anyMatch(Predicate<? super T> predicate);
boolean allMatch(Predicate<? super T> predicate);
boolean noneMatch(Predicate<? super T> predicate);
Optional<T> findFirst();
Optional<T> findAny();
T reduce(T identity, BinaryOperator<T> accumulator);
Object[] toArray();
<R, A> R collect(Collector<? super T, A, R> collector);
看到这些方法是不是很眼熟???
没错,在讲Lambda表达式和函数式接口的时候,我们就列出过一些函数式接口的表格,我再把它贴出来吧,不是凑字数哦!
接口名 | 参数 | 返回值 | 用途 | 含义 |
---|---|---|---|---|
Predicate | T | boolean | 断言 | 代表一个输入 |
Consumer | T | void | 消费 | 代表一个输入 |
Function<T,R> | T | R | 函数 | 代表一个输入,一个输出(一般输入和输出是不同类型的) |
BiFunction<T,U,R> | (T,U) | R | 函数 | 代表两个输入,一个输出(一般输入和输出是不同类型的) |
Supplier | None | T | 工厂方法 | 代表一个输出 |
UnaryOperator | T | T | 逻辑非、迭代器 | 代表一个输入,一个输出(输入和输出是相同类型的) |
BinaryOperator | (T,T) | T | 二元操作 | 代表两个输入,一个输出(输入和输出是相同类型的) |
不知道有没有人和我一样,第一次学习Lambda表达式的时候,根本不理解这些函数式接口的用途代表什么意思。比如什么是“断言”,什么是“消费”等等。
所以,我们还是有必要了解一下这些概念的,这对于我们之后的学习和学术讨论很有帮助。
断言:其实是防止程序意外出错的一种宏,如果其参数计算为假,则程序发出警告,且退出。
最常见的用法就是在函数入口处保证输入参数的正确性。
assert(object != NULL) ;
消费:是指程序在运行过程中,资源的消耗和使用。
最常用的用法就是将元素传递给其他函数处理,并且不给与返回值。
stream.forEach(System.out::println)
函数:也称函数关系式,是指给定元素,经过一些行为、处理后得到其他的元素的关系表达,它的核心就是对应关系。
函数的用途就很广了,尤其是我们学习函数式编程之后,才能真正体会它的强大。
stream.map {
x -> x.toString
}
工厂方法:我们知道工厂是生成东西的工具,那么我们是不是可以理解为工厂方法是生成方法的工具呢?是的,说明白一点就是提供一些生成对象的方法给我们使用,它本身是不创建东西的,我们只需要关心方法如何使用。
stream.collect(Collectors.toList())
可以看到 Collectors
类含有很多生成其他对象的方法,比如 toCollection()
、toSet()
、toList()
、toMap()
等等。
逻辑非:顾名思义,就是取一个有效值的反值。
使用场景多用于输入和输出的类型相同。
Stream.iterate(1, x -> !x)
二元操作:即为输入两个参数和输出的类型相同,多用于合并 Stream
的场景。
stream.reduce((value1, value2) ->
value1 + value2
)
了解这些函数式接口之后,我们在使用 Stream API
的时候就很容易了,接下来我们来看看这些操作符的用法吧。
filter 过滤
filter中的条件,返回true的才会保留
// 过滤基数值,保留偶数值,最后遍历
Arrays.asList(1, 2, 3, 4, 5).stream().filter(x -> x%2 == 2).forEach(System.out::println);
distince 去重
distinct是调用元素的hashCode和equals方法判断重复的值或者对象,从而实现去重功能。
对于实体类去重时,需要重写实体类的 equals()
以及 hashCode()
方法
也可以使用 filter
、collect
操作符去重。
//去重,barry存在2个,去重后只有一个
Stream.of("wally", "barry", "rose", "barry").distinct().forEach(System.out::println);
// filter 实体类字段去重
private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
Set<Object> seen = ConcurrentHashMap.newKeySet();
return t -> seen.add(keyExtractor.apply(t));
}
stre.filter(distinctByKey(User::getName)).forEach(System.out::println);
// collect 实体类字段去重
stream.collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(User::getName))), ArrayList::new)).forEach(System.out::println);
sorted 排序
默认按照自然顺序,从小到大排序。
Stream.of(1, 4, 8, 2, 0, -1).sorted().forEach(System.out::println);
// 倒序排序
Stream.of(1, 4, 8, 2, 0, -1).sorted((a, b) -> b - a).forEach(System.out::println);
limit 限制
限制取出 Stream
的个数。
Stream.iterate(1, x -> x + 1).limit(50).forEach(System.out::println);
skip 步长
跳过步长大小取值。
Stream.iterate(1, x -> x + 1).skip(5).limit(50).forEach(System.out::println);
map 数据转换
// 将Stream的每个元素进行大写转换
Stream.of("wally", "barry", "rose", "barry").map(String.toUpperCase).forEach(System.out::println);
flatMap 摊平
组合多个流为一个流
Stream.of(Arrays.asList("wally", "barry", "rose"), Arrays.asList("jack", "ben")).flatMap(names -> names.stream).forEach(System.out::println);
peek 查看
这个操作符主要目的是用于调试,查看 Stream
中的数据经过每个操作符后的状态。
Stream.of("wally", "barry", "rose", "barry")
.peek(System.out::println)
.map(String.toUpperCase)
.peek(System.out::println)
.forEach(System.out::println);
需要注意一点,peek是中间操作符,直接使用是无效的,不会打印任何东西。
parallel 并行
使用该操作符,会把当前 Stream
用并行方式处理。
设置并行 Stream
的线程数(包含主线程)。
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "5")
sequential 串行(默认)
使用该操作符,会把当前 Stream
用串行方式处理。
forEach 遍历
Stream.of("wally", "barry", "rose", "barry").forEach(System.out::println);
min 取最小
int min = Stream.of(1, 2, 3, 4, 5, 6).min(Integer::compareTo).orElse(-1);
max 取最大
int max = Stream.of(1, 2, 3, 4, 5, 6).max(Integer::compareTo).orElse(-1);
count 计数
统计元素个数
long count = Stream.of(1, 2, 3, 4, 5, 6).count();
average 取平均
取平均只有IntStream、DoubleStream、LongStream中可以使用,也可以使用collect取平均值
Double averagingValue = Stream.of(1, 2, 3, 4, 5, 6).collect(Collectors.averagingInt(value -> value));
anyMatch 找任意匹配条件的值或对象
判断,只要一个匹配,就返回true
// 匹配长度大于或者等于 5,只要有一个匹配,即使 rose 的长度为 4 不符合也返回 true。
boolean result = Stream.of("wally", "barry", "rose").anyMatch(s -> s.length() >= 5);
allMatch 找所有匹配条件的值或对象
判断,所有都匹配,才返回true
// 匹配长度大于或者等于 5,因为 rese 的长度 4,所以返回false
boolean result = Stream.of("wally", "barry", "rose").allMatch(s -> s.length() >= 5);
noneMatch 找不符合匹配条件的值或对象
// 匹配长度大于或者等于 5,因为 wally、barry 的长度符合,所以返回false
boolean result = Stream.of("wally", "barry", "rose").noneMatch(s -> s.length() >= 5);
findFirst 找第一个值或对象
Optional<Integer> first = Stream.of(1, 2, 3, 4, 5, 6).findFirst();
findAny 找任意的值或对象
Optional<Integer> any = Stream.of(1, 2, 3, 4, 5, 6).findAny();
reduce 累积、结合
根据一定的规则将 Stream
中的元素进行计算后返回一个唯一的值。
有三种方法可以调用,分别是:
- Optional<T> reduce(BinaryOperator<T> accumulator);
- T reduce(T identity, BinaryOperator<T> accumulator);
- <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
需要注意的是,在使用三个参数的方法时,如果 Stream 是非并行的,combiner 不会生效,计算过程和两个参数的方法是一致的。
// 方法一,二元运算处理,常用于求和、求最小值或最大值。
Optional<Integer> reduce = Stream.of(1, 2, 3, 4, 5, 6).reduce(Integer::sum);
// 方法二,在方法一的基础上,增加初始值。常用于将一个 String 类型的 Stream 中的所有元素连接到一起并在最前面添加 value 后返回。
String reduce = Stream.of("Hello", " ", "World").reduce("Log:", (s1, s2) -> s1+ s2);
// 方法三,并行时,将 identity 参数与序列中的每一个元素进行 accumulator 转换,得到N个结果,然后使用 combiner 对N个结果进行汇总。
// 使用函数表示就是:(4+1) * (4+2) * (4+3) = 210;
int reduce = Stream.of(1, 2, 3).reduce(4, (v1, v2) -> v1 + v2, (v1, v2) -> v1 * v2);
reduce即使是并行情况下,也不会创建多个初始化对象,combiner接收的两个参数永远是同一个对象。
// array1 == array2 永远为True
Predicate<String> predicate = t -> t.contains("a");
ArrayList<String> list = Stream.of("aa", "ab", "c", "ad").parallel().reduce(new ArrayList<>(),
(array, s) -> {
if (predicate.test(s)) array.add(s);
return array;
},
(array1, array2) -> {
System.out.println(array1 == array2);
array1.addAll(array2);
return array1;
});
toArray 转换成数组
有两种方法可以调用,分别是:
- Object[] toArray();
- <A> A[] toArray(IntFunction<A[]> generator);
Integer[] array = Stream.of(1, 2, 3, 4, 5, 6).toArray(Integer[]::new);
collect 收集
collect含义与reduce有点相似,区别是reduce即使是并行情况下,也不会创建多个初始化对象,combiner接收的两个参数永远是同一个对象。
有两种方法可以调用,分别是:
- <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner);
- <R, A> R collect(Collector<? super T, A, R> collector);
// 方法一,和 reduce 的三个参数方法用法类似。
Predicate<String> predicate = t -> t.contains("a");
ArrayList<String> list = Stream.of("aa", "ab", "c", "ad").parallel().collect(ArrayList::new,
(array, s) -> {
if (predicate.test(s)) array.add(s);
},
ArrayList::addAll);
// 使用 Collectors 提供的 API
stream.collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(User::getName))), ArrayList::new)).forEach(System.out::println);