Java 流式编程小结
简介
什么是流?
PS: 需要注意到 Stream流式编程 和 I/O流(例如FileInputStream)是两个概念,请勿混淆
Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。
Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。
集合优化了对象的存储,大多数情况下,我们将对象存储在集合是为了处理他们。使用流可以帮助我们处理对象,无需迭代集合中的元素,即可直接提取和操作元素,并添加了很多便利的操作,例如查找、过滤、分组、排序等一系列操作。
流的一个核心好处是:它使得程序更加短小并且易于理解,当结合 Lambda 表达式和方法引用时,会让人感觉自成一体。总而言之,流就是一种高效且易于使用的处理数据的方式。
流的操作
Stream 的操作符大体上分为两种:
-
中间操作符
对于数据流来说,中间操作符在执行指定处理程序后,数据流依然可以传递给下一级的操作符。**如果Stream只有中间操作是不会执行的,当执行终端操作的时候才会执行中间操作,这种方式称为延迟加载或惰性求值。**多个中间操作组成一个中间操作链,只有当执行终端操作的时候才会执行一遍中间操作链。
-
终止操作符
数据经过中间加工操作,就轮到终止操作符上场了;
终止操作符就是用来对数据进行收集或者消费的,数据到了终止操作这里就不会向下流动了,终止操作符只能使用一次。
特点
- 流本身不存储元素,并且不会改变源对象,相反,它会返回一个持有结果的新流
- 流可以在不使用赋值或可变数据的情况下对有状态的系统建模
- 流是一种声明式编程风格,它声明想要做什么,而非指明如何做
- 流的迭代过称为内部迭代,你看不到迭代过程,可读性更强
- 流是懒加载的,它会等到需要时才执行
流的创建
-
我们可以通过
java.util.Colletion.stream()
方法用集合创建流例如:
1
2
3
4
5List<String> list = Arrays.asList("a", "b", "c");
//创建一个顺序流
Stream<String> stream = list.stream();
//创建一个并行流
Stream<String> parallelStream = list.parallelStream();PS:
stream和parallelStream的简单区分:
stream是顺序流,由主线程按顺序对流执行操作; parallelStream是并行流,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求。
例如筛选集合中的奇数,两者的处理不同之处:
并行流底层采用ForkJoinPool线程池执行分段任务,fork递归式的分解任务,然后分段并行执行,最终由join合并结果,返回最后的值。Fork/Join建立在ExecutorService之上,与传统的线程主要的区别在于如何在线程和支持多核的机器间分配工作; 用一个简单的ExecutorService你能完全控制工作线程之间的负载分布,确立每个任务的大小以便线程来处理;Fork/Join有个work-stealing算法用以分配线程间的负载,可以将大型任务可以被分成更小单元,并在不同的线程间处理。
-
我们可以使用
java.util.Arrays.stream(T[] array)
方法用数组创建流例如:
1
2int[] arr = new int[]{1, 3, 5, 6, 8};
IntStream stream = Arrays.stream(arr); -
我们可以使用Stream的静态方法:
of()
、iterate()
、generate()
、builder()
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 3).limit(4);
//Stream.iterate() 产生的流的第一个元素是种子,然后把种子传递给方法,方法的运行结果被添加到流,并作为下次调用 iterate() 的第一个参数
stream2.forEach(System.out::println); // 0 3 6 9
Stream<Double> stream3 = Stream.generate(Math::random).limit(3);
//使用 Stream.generate() 搭配 Supplier<T> 生成 T 类型的流
stream3.forEach(System.out::println);
//使用 Stream.generate() 和 Stream.iterate() 生成的无限流一定要用 limit() 截断
Stream.Builder<String> builder = Stream.builder();
//使用建造者模式创建一个 builder 对象,然后将创建流所需的多个信息传递给它,最后 builder 对象执行创建流的操作
builder.add("a");
builder.add("b");
builder.build(); // 创建流
// builder.add("c") // 调用 build() 方法后继续添加元素会产生异常
流的中间操作
map
Stream<R> map(Function<? super T, ? extends R> mapper)
map
的作用,简单来说就是把 input Stream 的每一个元素,映射成 output Stream 的另外一个元素。将一个流转化为一个新的流,是惰性操作。
如图:
例如转换大小写:
1 | List<String> collected = Stream.of("a", "b", "hello") |
PS: 传给map的参数是一个lambda表达式,该lambda表达式必须是Function结构的实例,即lambda表达式只接受一个参数,并且返回一个值,接受参数类型和返回参数的类型可以不一样。
flatMap
Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
flatMap
的作用和map
有些类似,同样也是用于生成流。通过接收一个Function函数作为参数,将流中的每个值都转换成另一个流,然后把所有流连接成一个流。如图:
例如将多个List进行合并:
1 | Stream<List<Integer>> inputStream = Stream.of( |
PS: 上面的代码使用Stream的工厂方法,将每一个列表转换为Stream对象,然后使用flatMap方法将多个Stream转换为一个新的Stream。
limit
Stream<T> limit(long maxSize)
在上文的流的创建示例中我们也接触过了limit(long)
方法,他将生成的无限流进行了一个截断,我们也将其称为截断流,使其元素不超过给定数量。如果元素的个数小于maxSize
,那就获取所有元素。
例如生成3个随机数并装入数组:
1 | Stream<Double> stream = Stream.generate(Math::random).limit(3); |
我们直接将generate()
方法生成的无限流在头处进行截断,很直观明了。
skip
Stream<T> skip(long n)
skip
方法同limit
方法类似,只是limit
方法是从头处开始进行截断的,而skip
方法是跳过流的头部若干个元素,选择剩下的。跳过元素,返回一个扔掉了前 n
个元素的流。若流中元素不足 n
个,则返回一个空流。我们可以认为这是一种和limit
方法的互补。如图:
distinct
Stream<T> distinct()
distinct
顾名思义就是去重,通过流所生成元素的 hashCode()
和 equals()
去除重复元素。
例如过滤重复数组:
1 | List<Integer> list = Arrays.asList(1, 1, 2, 2, 3, 3, 4, 4, 5, 5); |
最终我们就能达到1,2,3,4,5去重的目的
filter
Stream<T> filter(Predicate<? super T> predicate)
filter
函数也是顾名思义,就是将当前流的元素进行一个过滤,筛选出满足条件的元素。
Predicate
函数是断言型接口,在filter
方法中是接收一个和Predicate
函数对应Lambda表达式,返回一个布尔值,从流中过滤某些元素。如图:
例如我们过滤出数组中大于5的元素:
1 | List<Integer> list = Arrays.asList(1, 5, 6, 2, 4, 7, 9, 10); |
sorted
Stream<T> sorted(Comparator<? super T> comparator)
根据简单含义,我们也能理解出sorted
方法可以对流中的元素根据比较器进行一个重新的排序。
例如从大到小排序数组:
1 | List<Integer> list = Arrays.asList(1, 5, 6, 2, 4, 7, 9, 10); |
流的终端操作
经常使用的终端操作主要有:
forEach
void forEach(Consumer<? super T> action)
也就是内部迭代操作,通过forEach
函数,我们将流中的元素按照顺序迭代遍历,并执行相应的action
操作。
最典型的例子就是将流中的元素进行打印:
1 | List<Integer> list = Arrays.asList(1, 5, 6, 2, 4, 7, 9, 10); |
结果如下:
1 | 10 |
collect
<R, A> R collect(Collector<? super T, A, R> collector)
在上方很多样例中,我们已经使用到了collect
方法,通过他我们可以实现将流转化为集合等功能。collect
方法功能是:收集、将流转换为其他形式,比如转换成List、Set、Map。collect
方法是用Collector
作为参数,Collector
接口中方法的实现决定了如何对流执行收集操作(如收集到 List
、Set
、Map
)。但是 Collectors
实用类提供了很多静态方法,可以方便地创建常见收集器实例。
常用的收集器可从java.util.stream.Collectors中导入,例如Collectors.toList()就是一种从流生成对应列表的收集器,通过将收集器传递给collect方法,所有的流就可以使用它了。
例如:
1 | List<User> users = Lists.newArrayList(); |
其他终端操作
boolean allMatch(Predicate<? super T> predicate);
检查是否匹配所有元素。boolean anyMatch(Predicate<? super T> predicate);
检查是否至少匹配一个元素。boolean noneMatch(Predicate<? super T> predicate);
检查是否没有匹配所有元素。Optional<T> findFirst();
返回当前流中的第一个元素。Optional<T> findAny();
返回当前流中的任意元素。long count();
返回流中元素总数。Optional<T> max(Comparator<? super T> comparator);
返回流中最大值。Optional<T> min(Comparator<? super T> comparator);
返回流中最小值。T reduce(T identity, BinaryOperator<T> accumulator);
可以将流中元素反复结合起来,得到一个值。 返回 T。这是一个归约操作。
参考: