【Java集合框架】3 - 16 Stream 流

§3-16 Stream

3-16.1 Stream 流的获取方法

Stream 是一个位于 java.util.stream 包下的接口。

该接口的思想类似于流水线,流水线对原料逐步进行各种操作,原料经历完整的流水线后,形成最终的产品,离开流水线。

Steam 流借鉴了这种思想,提供了许多方法作为 “处理原料的操作”,可称为中间方法(例如过滤、转换等),最终 “原料形成产品离开流水线”,可称为终结方法(例如输出、统计等)。

中间方法允许持续地进行链式调用其他方法,而终结方法是流的最后一个执行方法,执行完后不可再调用流的其他方法。

Stream 流的使用步骤

  1. 先得到一条 Stream 流(流水线),并将数据放上去;
  2. 利用 Stream 中的 API 进行各种操作;

获取方法

获取来源 方法 描述
Collection 单列集合接口 default Stream<E> stream() 返回一条以指定单列集合为源的顺序流
Map 双列集合接口 \ 双列集合无法使用 Stream
Arrays 数组工具类 Stream<T> stream(T[] array) 返回一条以指定数组为源的顺序流
Stream 接口 Stream<T> of(T... values) Stream 接口中的静态方法,以指定数据为源获取流

注意

  • 一般而言,我们不会用独立变量接收流对象,而是直接采用链式编程,用连点式不断地调用方法;

  • 双列集合无法直接使用 Stream 流,应当先获取键或键值对的单列集合,再通过单列集合获取流;

  • Stream.of 方法的形参是一个可变参数,本质上是一个数组;若传递的是基本数据类型数组,则不会自动装箱,反而会将整个数组而不是其中的数据放入流水线中;

    int[] arr1 = {1, 2, 3, 4, 5};
    String[] arr2 = {"aaa", "bbb", "ccc", "ddd", "eee"};
    
    //基本数据类型
    Stream.of(arr1).forEach(i -> System.out.println(i));	//[I@1d81eb93:数组地址
    //引用数据类型
    Stream.of(arr2).forEach(e -> System.out.println(e));
    

3-16.2 中间方法

中间方法就好比原料在流水线上的加工。

常用方法

方法 描述
Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) 返回一个弱连接的流,其中后者连接在前者后面
Stream<T> distinct() 返回一条不同元素的流
Stream<T> filter(Predicate<? super T> predicate) 返回一条匹配指定谓词元素的流(过滤)
Stream<T> limit(long maxSize) 将该流截短至指定长度后返回(限制)
Stream<R> map(Function<? super T, ? extends R> mapper) 对流中的元素应用指定函数后,返回含有该结果的流
Stream<T> skip(long n) 返回一条丢弃前 n 个元素的流
Stream<T> sorted() 返回一条经过自然排序的流
Stream<T> sorted(Comparator<? super T> comparator) 返回一条经比较器排序的流

注意

  • 中间方法返回的是一个新的流对象,原来的流只能使用一次,记录为单独的变量没有意义,建议使用链式编程;
  • 在流上对数据元素所进行的任何修改均不会反映到原始数据源;
  • 应当尽可能地保证 concat 方法中的两条流中的数据类型相同,不相同则会转换为二者的父类,这样会使得子类的特有方法无法被调用;
  • distinct 方法可用于元素去重,其底层使用了 HashSet,使得元素去重依赖于 hashCodeequals 方法;
  • Predicate 是一个位于 java.util.function 的函数式接口,表示一个单参数的谓词(布尔值函数),该方法为 test(Object),方法返回 true 表示该元素留下,反之舍弃;
  • Function 是一个位于 java.util.function 的函数式接口,表示一个接受单参数且产生结果的一个函数,方法为 apply(Object)

3-16.3 终结方法

终结方法好比原料最后的出厂。

常用方法

方法 描述
long count() 返回流上元素的个数
R collect(Collector<? super T,A,R> collector) 使用收集器对流上的每一个元素执行动态规约运算(收集到集合中)
void forEach(Consumer<? super T> action) 对流上的每个元素执行操作
Object[] toArray() 返回一个含有流上所有元素的数组(收集到数组中)
A[] toArray(IntFunction<A[]> generator) 返回一个含有流上所有元素的数组(收集到数组中)
List<T> toList() 返回一个含有流上所有元素的列表

注意

  • 上述方法的返回值不再是流,因此上述方法用于链式编程的最后一个方法调用;

  • Collector 是一个位于 java.util.stream 的接口,动态规约(mutable reduction)运算将输入元素累积到一个可变的结果容器中,当所有输入元素都已处理后,将所积累的结果转换为最终表示(可选);

  • Collector<T,A,R> 中的泛型依次表示执行规约运算的输入元素类型、规约运算的所积累元素的类型(通常隐藏为实现细节)、规约运算的结果类型;

  • 一般而言,会调用工具类 Collectors 中的对应方法,这些方法返回收集器,可用作 collect 的参数,该类位于 java.util.stream 下;

  • IntFunction<R>java.util.function 的一个函数式接口,表示一个接受单整型值参数并产生结果的一个函数,其方法为 apply

  • IntFunction 的匿名类泛型为 <? extends Object[]> (可根据实际情况手动更改,但必须为数组),该泛型表示某个类型的数组,toArray 以该接口的实现类对象为参数,用于创建一个指定类型数组,而 toArray 底层会依次得到流上的每一个数据,并放到数组中;

    String[] arr = names.stream().toArray(new IntFunction<String[]>() {
        @Override
        public String[] apply(int value) {
            //value表示流上元素个数
            return new String[value];
        }
    });
    

3-16.4 collect 使用方法

Stream 中的 collect 方法提供了从流转换到集合的方法。

现有如下集合,以下的转换都基于此集合进行:

ArrayList<String> names = new ArrayList<>();
Collections.addAll(names, "张无忌-男-15", "周芷若-女-14", "赵敏-女-13", "张强-男-20"
                   , "张三丰-男-100", "张翠山-男-40", "张良-男-35", "王二麻子-男-37", "谢广坤-男-41");
//要求先过滤出男性成员信息

转换到列表

使用 Collectors.toList() 即可。

List<String> list = names.stream()
    //尽量使用固定数据来调用方法,这样可以尽力地避免空指针异常
    .filter(s -> "男".equals(s.split("-")[1]))
    .collect(Collectors.toList());
System.out.println(list.getClass());    //ArrayList
System.out.println(list);
System.out.println("==================");

转换到集合

使用 Collectors.toSet() 即可,方法会自动去重。

Set<String> set = names.stream()
    .filter(s -> "男".equals(s.split("-")[1]))
    .collect(Collectors.toSet());
System.out.println(set.getClass());     //HashSet
System.out.println(set);
System.out.println("==================");

转换到映射表

使用 Collectors.toMap() 即可,方法传入两个参数,都为 Function 接口实现类对象,分别用于指定键和值的生成规则。

接口泛型参数:记住所表示的一一对应关系即可

  • 两个接口都具有两个泛型,其中第一个泛型表示流的元素类型;
  • 对于第一个接口,其第二个泛型指的是键的类型。内部方法形参来自流的元素,返回键;
  • 对于第二个接口,其第二个泛型指的是值的类型。内部方法形参来自流的元素,返回值;

注意:键不可重复,若发现重复的键,则会抛出异常 duplicateKeyException

Map<String, Integer> map = names.stream()
    .filter(s -> "男".equals(s.split("-")[1]))
    .collect(Collectors.toMap(new Function<String, String>() {
        @Override
        public String apply(String s) {
            //键
            return s.split("-")[0];
        }
    }, new Function<String, Integer>() {
        @Override
        public Integer apply(String s) {
            //值
            return Integer.parseInt(s.split("-")[2]);
        }
    }));

改写成 Lambda 表达式:

Map<String, Integer> map = names.stream()
    .filter(s -> "男".equals(s.split("-")[1]))
    .collect(Collectors.toMap(s -> s.split("-")[0], s -> Integer.parseInt(s.split("-")[2])));
System.out.println(map.getClass());     //HashMap
System.out.println(map);
System.out.println("==================");

3-16.5 Stream.Builder 流构建器

由名字可知,流构建器是 Stream 的内部接口,其功能用于构建一个流,以供后续在流上的聚合操作。

获取方法:通过 Stream 接口的静态方法获得。

静态方法 描述
Stream.Builder<T> builder() 返回流的构建器

获得流的构建器后,就可以使用流的构建器构建流。流的构建器通过一个个生成元素并将其添加到构建器中创建流,这节省了使用 ArrayList 作为临时缓存的复制开销。

流构建器具有生命周期。生命周期起始于构建期,该阶段允许向构建器添加元素。然后过渡到构建完成期,在这之后不再允许向构建器添加元素。构建器再调用 build() 方法后过渡到构建完成期,这时构建器会返回一个流,流中元素的顺序同元素的添加顺序。

构建器的方法

方法 描述
void accept(T t) 在构建期中,向构建器添加元素
Stream.Builder<T> add(T t) 在构建期中,向构建器添加元素,返回该构建器本身
Stream<T> build() 构建流,并将构建器过渡到构建完成的状态

注意

  1. add(T) 方法:方法的默认实现行为是:

    accept(t);
    return this;
    
  2. build() 方法:调用该方法后,任何进一步操作该构建器的尝试都会导致 IllegalStateException

java.util.stream 包中还有一些专门化的流,这些流也有自己的构建器,这些构建器的行为都是一致的,具有生命周期,都可以用于构建指定的流。9

3.19.6 stream 包中的基本类型序列流

java.util.stream 包中提供了三种基本类型的序列流,分别是 IntStream, DoubleStreamLongStream 接口。这些接口支持顺序和并行聚合操作,是对应基本类型的专门化 Stream

这些流在专门处理大量对应基本类型的数据时十分有用。

获取方法:有三种获取对应基本类型序列流的方法。

  1. 直接调用对应接口的静态方法,构建对应流;
  2. 通过 Stream 中的映射方法,将流转化为序列流;
  3. 通过已有的序列流中的成员方法,将流转换为新的序列流;

这里重点介绍第一种获取方法,但只列出部分方法。

所属接口 静态方法 描述
IntStream IntStream.Builder builder() 返回 IntStream 的构建器
DoubleStream DoubleStream.Builder builder() 返回 DoubleStream 的构建器
LongStream LongStream.Builder builder() 返回 LongStream 的构建器
各自接口中 IntStream concat(IntStream a, IntStream b)
DoubleStream concat(DoubleStream a, DoubleStream b)
LongStream concat(LongStream a, LongStream b)
创建一个弱连接的流,流中元素顺序为 a 先 b 后
各自接口中 empty() 创建一个空的流
各自接口中 IntStream generate(IntSupplier s)
DoubleStream generate(DoubleSupplier s)
LongStream generate(LongSupplier s)
创建一个无限无序的流,元素由对应类型提供者提供,可用于创建持续流或随机元素的流
各自接口中 of(int/double/long t)
of(int/double/long... value)
返回含有指定元素的有序流
IntStreamLongStream IntStream range(int startInclusive, int endExclusive) 返回一个有序流,含有指定左闭右开区间范围内的元素,步长为 1
IntStreamLongStream IntStream rangeClosed(int startInclusive, int endInclusive) 返回一个有序流,含有指定闭合区间范围内的元素,步长为 1

成员方法

序列流支持非常多的操作,因而有非常多的支持方法,这里只列出一部分,详见官方文档。

所有的序列流都支持 Stream 的方法,如 allMatch, anyMatch, collect, filter, findAny, min, max, sorted 等,这里不再列出。

这里仅以 IntStream 为例。

方法 描述
DoubleStream asDoubleStream() 将流转换为 DoubleStream
LongStream asLongStream() 将流转换为 LongStream
OptionalDouble average() 返回流中所有元素的算术平均值,若流为空,则返回空可选值
Stream<Integer> boxed() 返回包含流中所有元素的普通流,所有元素都被装箱为包装类对象
int sum() 返回流中所有元素的和
IntSummaryStatistics summaryStatistics() 返回描述流中元素多个概要数据的 IntSummaryStatistics

示例

package com.jcf.stream;

import java.util.IntSummaryStatistics;
import java.util.IntStream;

public class IntStreamDemo {
    public static void main(String[] args) {
        // 调用静态方法直接创建流
        System.out.println(IntStream.rangeClosed(0, 100).sum());
        IntSummaryStatistics stats = IntStream.of(10, 15, 13, 5, 20, 0).sorted().summaryStatistics();
        System.out.println("最大值:" + stats.getMax());
        System.out.println("最小值:" + stats.getMin());
        System.out.println("个数:" + stats.getCount());
        System.out.println("平均值:" + stats.getAverage());
        System.out.println("总和:" + stats.getSum());
        System.out.println(stats);
    }
}

运行结果:

5050
最大值:20
最小值:0
个数:6
平均值:10.5
总和:63
IntSummaryStatistics{count=6, sum=63, min=0, average=10.500000, max=20}
posted @ 2023-08-14 21:12  Zebt  阅读(40)  评论(0)    收藏  举报