初识 Stream
Java 8 API添加了一个新的抽象称为流 Stream,可以让你以一种声明的方式处理数据。使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。
Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。
图上的流程转换为 Java 代码为:
List<Integer> transactionsIds =
widgets.stream() // Source
.filter(b -> b.getColor() == RED) // Operations-filter
.sorted((x,y) -> x.getWeight() - y.getWeight()) // Operations-sorted
.mapToInt(Widget::getWeight) // Operations-map
.sum(); // Results-sum
什么是 Stream?
Stream(流)是一个来自数据源的元素队列并支持聚合操作
- 无存储:不是一种数据结构,它只是某种数据源的一个视图,数据源可以是一个数组,Java容器或I/O channel等。
- 为函数式编程而生:对 stream 的修改都不会修改其数据源,比如对 stream 执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新 stream。
(peek 对可变对象可以修改)
- 惰式执行:stream 上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
- 可消费性:stream 只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。
Stream操作还有两个基础的特征:
- Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
- 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。
关于 Stream 的相关API实现原理, 感兴趣的可以去看下: 浅析 Java8 Stream 原理
创建流
stream 常见的创建的方式主要有Arrays、Collection、Stream 静态方法等,这里代码列举其中最常见的几种:
package com.company.designModel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.stream.Stream;
public class Java8StreamTest {
public static void main(String[] args) {
// 1.数组创建流
Integer[] arrays = {9, 5, 2, 7};
Stream<Integer> arraysStream = Arrays.stream(arrays);
// 2.Collection 创建流
ArrayList<Object> list = new ArrayList<>();
list.add("Amy");
list.add("Angst");
Stream<Object> stream = list.stream();
Stream<Object> parallelStream = list.parallelStream(); // 这里 parallelStream 创建的是一个并行流
// 3. Stream.of
Stream.of(1, 24, 8, 6, 10, 4, 8, 3, 2, 8, 6).skip(2).limit(5).forEach(System.out::println);
// empty() 可以创建一个空的流
Stream<Object> emptyStream = Stream.empty();
// 4.Stream.generate()
Stream.generate(Math::random).limit(10).skip(1).forEach(System.out::print);
// 5.Stream.iterate()
Stream.iterate(0, item -> ++ item).limit(10).forEach(System.out::print);
// 6.Stream.builder()
Stream<Object> buildStream = Stream.builder().add("Amy").add("Angst").build();
// 7. Stream.concat() 合并创建一个新的流
Stream<Object> concatStream = Stream.concat(stream, parallelStream);
}
}
这里有几个需要关注的点:
- 查看 Stream 源码的话,你会发现 of() 方法内部其实调用了 Arrays.stream() 方法。源码注释有提到
“从数组创建的流是安全的”
/**
* Returns a sequential ordered stream whose elements are the specified values.
*
* @param <T> the type of stream elements
* @param values the elements of the new stream
* @return the new stream
*/
@SafeVarargs
@SuppressWarnings("varargs") // Creating a stream from an array is safe
public static<T> Stream<T> of(T... values) {
return Arrays.stream(values);
}
- Stream.generate(): 生成一个无限长度的Stream,其元素的生成是通过给定的Supplier(这个接口可以看成一个对象的工厂,每次调用返回一个给定类型的对象), 其中值是随机的。这个无限长度 Stream 是懒加载,一般都会配合Stream的limit()方法来做分页使用。
- Stream.iterate(): 也是生成无限长度的Stream,和 generator 不同的是,其元素的生成是重复对给定的种子值(seed)调用用户指定函数来生成的. 其中包含的元素可以认为是:seed,f(seed), f(f(seed))无限循环。
操作流
流的操作类型分为两种:
-
中间操作(Intermediate):一个流可以后面跟随零个或多个intermediate操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用(链式操作)。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
- 有状态(Stateless):指该操作只有拿到所有元素之后才能继续下去
- 无状态(Stateful):指元素的处理不受之前元素的影响
-
终止操作(Terminal):一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以,这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。
- 非短路操作: 指必须处理所有元素才能得到最终结果
- 短路操作: 指遇到某些符合条件的元素就可以得到最终结果,如 A || B,只要A为true,则无需判断B的结果
代码演示
Intermediate方法
- filter(): 用于筛选数据,返回由匹配的元素组成的流。
- limit(): 切片限流可以当做分页来使用,返回被截断的流。
- skip():要跳过前面元素n的数量,返回由该流的剩余元素组成的流。
- distinct():去重操作,返回由不同元素组成的流。
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Vivian");
list.add("Amy");
list.add("Amy");
list.add("Angst");
list.add("Zx");
list.stream(). // 创建一个list流
distinct(). // 去重操作
skip(1). // 跳过第一个元素
filter(o -> o.startsWith("A")). // 过滤以A开头的元素
limit(2). // 切片选择保留几个
forEach(System.out::println); // 输出结果
}
结果输出:
Amy
Angst
Process finished with exit code 0
- map():接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
- flatMap():接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Vivian");
list.add("Amy");
list.add("Angst");
// 1.map
list.stream().map(String::toLowerCase).forEach(System.out::println);
list.stream().mapToInt(String::length).forEach(System.out::println);
list.stream().mapToDouble(String::length).forEach(System.out::println);
list.stream().mapToLong(String::length).forEach(System.out::println);
// 2. flatMap 同 map 作用类似,区别就是将每个元素重新组成Stream,并将这些Stream 串行合并成一条Stream.
list.stream().flatMap(o-> Stream.of(o.toLowerCase())).forEach(System.out::println);
list.stream().flatMapToInt(o-> IntStream.of(o.length())).forEach(System.out::println);
list.stream().flatMapToDouble(o-> DoubleStream.of(o.length())).forEach(System.out::println);
list.stream().flatMapToLong(o-> LongStream.of(o.length())).forEach(System.out::println);
}
结果输出:
vivian
amy
angst
6
3
5
6.0
3.0
5.0
6
3
5
vivian
amy
angst
6
3
5
6.0
3.0
5.0
6
3
5
Process finished with exit code 0
- sorted():自然排序,流中元素需实现Comparable接口, 也可以自定义 Comparator
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("Vivian");
list.add("Amy");
list.add("Angst");
list.stream().sorted().forEach(System.out::println); // 默认排序
list.stream().sorted((x, y) -> y.length() - x.length()).forEach(System.out::println); // 自定义排序
}
结果输出:
Amy
Angst
Vivian
Vivian
Angst
Amy
Process finished with exit code 0
- peek():如同于map(),能得到流中的每一个元素。但map接收的是一个 Function 表达式,有返回值。而peek接收的是Consumer,没有返回值。
public class Java8StreamTestDemo {
public static void main(String[] args) {
// 1.不可变对象
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Vivian");
arrayList.add("Amy");
arrayList.add("Angst");
// 修改元素首字母小写,实际结果并没有修改成功
arrayList.stream().peek(String::toLowerCase).forEach(o -> System.out.println("toLowerCase:" + o));
// 2.可变对象
ArrayList<Food> list = new ArrayList<>();
list.add(new Food("果汁"));
list.add(new Food("奶茶"));
list.add(new Food("牛奶"));
// peek()方法存在的主要目的是用调试,通过 peek() 方法可以看到流中的数据经过每个处理点时的状态。
list.stream().peek(o -> System.out.println("debugger: "+ o.getCoca())).count();
// 除去用于调试,peek()在需要修改元素内部状态的场景也非常有用,比如修改 Coca 的值 ,当然也可以使用map()和flatMap实现.
list.stream().peek(o->o.setCoca("饭前:"+ o.getCoca())).forEach(System.out::println);
}
// 定义一个可变对象
static class Food {
private String coca="饮料";
public Food(String coca) { this.coca = coca; }
public String getCoca() { return coca; }
public void setCoca(String coca) { this.coca = coca; }
@Override
public String toString() {
return "Food{" +
"coca='" + coca + '\'' +
'}';
}
}
}
结果输出:
toLowerCase:Vivian
toLowerCase:Amy
toLowerCase:Angst
debugger: 果汁
debugger: 奶茶
debugger: 牛奶
Food{coca='饭前:果汁'}
Food{coca='饭前:奶茶'}
Food{coca='饭前:牛奶'}
Process finished with exit code 0
注意
:map函数对Stream中元素执行的是映射操作,会以新的元素(map的结果)填充新的Stream。严格的讲map不是修改原来的元素。peek只能消费Stream中的元素,是否可以更该Stream中的元素,取决于Stream中的元素是否是不可变对象。如果是不可变对象,则不可修改Stream中的元素;如果是可变对象,则可以修改对象的值,但是无法修改对象的引用。
Terminal方法
- max(): 返回此流的最大元素
- min(): 返回此流的最小元素
- count(): 返回此流中的元素计数
- findFirst(): 返回此流中第一个元素
- findAny(): 取任意一个元素,正常情况下一般会取第一个元素,在并行流的情况下会随机取一个元素
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Vivian");
arrayList.add("Amy");
arrayList.add("Angst");
System.out.println(arrayList.stream().map(String::length).max(Integer::compareTo));
System.out.println(arrayList.stream().map(String::length).min(Integer::compareTo));
System.out.println(arrayList.stream().map(String::length).count());
System.out.println(arrayList.stream().map(String::length).findFirst());
// 在 parallel 流中存在每次输出的结果不一致
System.out.println(arrayList.stream().parallel().map(String::length).findAny());
}
结果输出:
Optional[6]
Optional[3]
3
Optional[6]
Optional[3]
Process finished with exit code 0
- andMatch(): 返回流中的元素是否与所提供的匹配
- allMatch(): 返回流的所有元素是否与指定的条件匹配。
- noneMatch(): 返回流中是否没有匹配谓词的元素
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Vivian");
arrayList.add("Amy");
arrayList.add("Angst");
// 匹配成功返回 true, 否则 false
System.out.println(arrayList.stream().noneMatch(o->o.equals("amy"))); // 空匹配
System.out.println(arrayList.stream().anyMatch(o->o.equals("Amy"))); // 任意匹配
System.out.println(arrayList.stream().allMatch(o->o.equals("Amy"))); // 全匹配
}
结果输出:
true
true
false
Process finished with exit code 0
- reduce(): 用于对 stream 中元素进行聚合求值
// 规约操作
System.out.println(Stream.of(1, 9, 5, 2, 7, -1).reduce(0, Integer::sum));
23
Process finished with exit code 0
- forEach(): 遍历元素
- iterator(): 返回一个迭代器
public static void main(String[] args) {
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("Vivian");
arrayList.add("Amy");
arrayList.add("Angst");
// 遍历元素
arrayList.stream().forEach(System.out::println);
// 返回的是一个可迭代对象
System.out.println(arrayList.stream().iterator());
}
结果输出:
Vivian
Amy
Angst
java.util.Spliterators$1Adapter@7ba4f24f
Process finished with exit code 0
- toArray(): 返回一个数组
- collect(): 收集操作,将一个对象的集合转化成另一个对象的集合
// 把流转换成数组
Object[] toArray = Stream.of(1, 9, 5, 2, 7, -1).toArray();
// 使用系统提供的收集器可以将最终的数据流收集到 List Set Map等容器中
List<Integer> list = Stream.of(1, 9, 5, 2, 7, -1).collect(Collectors.toList());
最后
以上就是本次的学习分享,Stream 的熟练使用可以让你的代码更加简洁易于维护,以上案例中可能还涉及其他的知识点,比如函数式接口、lambda等,感兴趣的可以看下笔者其他的学习笔记。欢迎留言讨论。
网友评论