介绍

        Stream 是 Java 8 引入的函数式编程工具,用于高效处理集合数据。它类似于“数据管道”,将数据从源头(如集合、数组)经过一系列操作(过滤、转换等)后输出结果。stream通常与Lambda表达式相结合,用于简化集合与数组的操作。

        我们可以将其理解为一条流水线,将数据放到stream流后,我们可以对数据进行各种操作。

        使用Stream流分为三步:

  1. 从集合、数组或其他数据源生成一个 Stream 对象。
  2. 使用中间方法对stream流中的数据进行操作。
  3. 使用终结方法对stream流中的数据进行操作。

        其中中间方法调用完毕后,还可以调用其他方法,但终结方法一旦调用则不能再调用其他方法了。接下来我们依次来看这三个阶段:

获取stream流

        在不同的情况下,获取stream流的方式也有所不同:

获取方式 使用方法 说明
单列集合 default Stream<E> stream() Collection中的默认方法
双列集合

需通过keySet()或entrySet()转为单列集合后再使用

无法直接使用stream流

数组 public static <T> Stream<T> stream(T[] array) Arrays工具类中的静态方法
一堆零散数据 public static <T> Stream<T> of(T… values) Stream接口中的静态方法

单列集合 

        接下来我们依次来演示,首先是单列集合:

// 单列集合获取Stream流
// 创建一个ArrayList对象,用于存储Object类型的元素
ArrayList<Object> list = new ArrayList<>();
// 使用Collections.addAll()方法向集合中添加元素"a", "b", "c", "d", "e"
Collections.addAll(list, "a", "b", "c", "d", "e");
// 使用集合的stream()方法获取Stream流
Stream<Object> stream1 = list.stream();
// 使用forEach方法遍历Stream流中的元素,并打印每个元素
stream1.forEach(System.out::println);
//输出结果(为节省空间,换行全由空格替代,后文同理):
//a b c d e

        但这样写还是过于复杂,我们一般直接使用链式编程进行输出:

ArrayList<Object> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c", "d", "e");
// 使用集合的stream()方法获取Stream流,并直接通过forEach方法遍历打印每个元素
list.stream().forEach(System.out::println);

双列集合 

        然后来看双列集合。因为双列集合是无法直接使用stream流的,我们只能通过调用keySet()或者entrySet()方法,单独对其键或值进行操作:

        // 双列集合
        // 创建一个HashMap对象,用于存储String类型的键和Integer类型的值
        HashMap<String, Integer> hashMap = new HashMap<>();
        // 向HashMap中添加键值对
        hashMap.put("第一", 111);
        hashMap.put("第二", 222);
        hashMap.put("第三", 333);
        hashMap.put("第四", 444);
        hashMap.put("第五", 555);

        // 使用keySet()方法获取所有键的集合,并转换为Stream流,然后遍历打印每个键
        hashMap.keySet().stream().forEach(System.out::println);
        // 输出结果:第四 第三 第五 第一 第二
        // 使用values()方法获取所有值的集合,并转换为Stream流,然后遍历打印每个值
        hashMap.values().stream().forEach(System.out::println);
        // 输出结果:444 333 555 111 222
        // 使用entrySet()方法获取所有键值对的集合,并转换为Stream流,然后遍历打印每个键值对
        hashMap.entrySet().stream().forEach(System.out::println);
        // 输出结果:第四=444 第三=333 第五=555 第一=111 第二=222

数组 

        然后是数组,直接调用Arrays工具类中的静态方法即可,无论是基本类型的数组还是引用类型的数组都适用:

int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9}; // 定义一个包含1到9的整型数组
Arrays.stream(arr).forEach(System.out::println); // 将数组转换为Stream流,并逐个打印每个元素

零散数据 

        最后是零散的数据,无论是基础数据还是引用数据都可以,但要求这些数据必须是同一种数据:

// 使用Stream.of()方法创建包含1到5的整数Stream流,并逐个打印每个元素
Stream.of(1, 2, 3, 4, 5).forEach(System.out::println);
// 使用Stream.of()方法创建包含"一"到"五"的字符串Stream流,并逐个打印每个元素
Stream.of(“一”, “二”, “三”, “四”, “五”).forEach(System.out::println);

        同时因为Stream.of()的形参是一个可变参数,而可变参数的底层就是数组,所以该方法可以传递一堆零散的数据,也可以传递数组。
        但注意,传递的数组必须是引用数据类型,如果传递基本数据类型,系统会将整个数组当作一个元素放到Stream流中。

中间方法

        常用的中间方法有以下六个:

名称 说明
Stream<T> filter(Predicate<? super T> predicate) 过滤
Stream<T> limit(long maxSize) 获取前几个元素
Stream<T> skip(long n) 跳过前几个元素
Stream<T> distinct() 元素去重,依赖hashCode和equals方法
static <T> Stream<T> concat(Stream a, Stream b) 合并a和b两个流为一个流
Stream<R> map(Function<T, R> mapper) 转换流中的数据类型

注意:

  1. 元素去重时,如果流中是自定义对象,需先重写hashCode和equals方法
  2. 合并流时应保持两流中的数据类型相同,如果不同,大流上的数据类型则为其共同的父类
  3. 中间方法返回新的Stream流,原来的Stream流只能使用一次,建议使用链式编程
  4. 修改Stream流中的数据,不会影响原来集合或者数组中的数据 

过滤filter 

        先来看过滤,我们需要过滤list中以"张"开头,且长度为三的字符串:

        // 创建一个ArrayList集合并添加初始数据
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三丰");

        // 使用Stream流进行两次过滤操作,先过滤以"张"开头的名字,再过滤长度为3的名字
        // 使用匿名内部类实现Predicate接口
        list.stream()
                .filter(new Predicate<String>() {
                    @Override
                    public boolean test(String s) {
                        // 第一次过滤条件:名字以"张"开头
                        return s.startsWith("张");
                    }
                })
                .filter(new Predicate<String>() {
                    @Override
                    public boolean test(String s) {
                        // 第二次过滤条件:名字长度为3
                        return s.length() == 3;
                    }
                })
                .forEach(s -> System.out.println(s)); // 打印最终过滤结果

         上述代码虽然实现了所需功能,但仍旧较复杂,接下来我们使用lambda进行链式编程简化:

        // 创建一个ArrayList集合并添加初始数据
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三丰");

        // 使用Lambda表达式简化过滤逻辑,效果与上方代码相同
        list.stream()
                .filter(name -> name.startsWith("张")) // 第一次过滤:名字以"张"开头
                .filter(name -> name.length() == 3)  // 第二次过滤:名字长度为3
                .forEach(System.out::println);       // 使用方法引用打印结果

         结合上文注意中的"中间方法返回新的Stream流,原来的Stream流只能使用一次"。
        本例子中我们直接使用链式编程进行了两次过滤,那是否可以改为先使用Stream类型的变量来接收上一个filter()方法过滤的结果,再进行操作呢?

        Stream<String> stream1 = list.stream().filter(name -> name.startsWith("张"));
        Stream<String> stream2 = stream1.filter(name -> name.length() == 3);
        stream2.forEach(System.out::println);

        测试后发现输出结果和上文相同,但如果我们想在下文再用一次stream1,系统就会报错:stream has already been operated upon or closed。正因为每个stream流都只能使用一次,所以我们就没有必要使用变量记录该流,而是直接使用链式编程进行操作。

获取limit/跳过skip

        这两个都比较简单,传入对应的参数,即可获取X个/跳过X个参数,直接演示:

        // 创建一个ArrayList集合并添加初始数据
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三丰","张翠山","王二麻子","谢广坤");

        // 使用Stream流对集合进行操作
        list.stream()
                // 限制流中元素的最大数量为6个
                .limit(6)
                // 跳过流中的前3个元素
                .skip(3)
                // 打印剩余的元素
                .forEach(System.out::println);
    }
        //输出:张强 张三丰 张翠山

去重distinct

        直接调用该方法即可,无需传入任何参数,其会自动去除流中的重复元素:

    // 使用Stream流对集合进行操作
    list.stream()
        // 去除流中重复的元素(基于equals方法)
        .distinct()
        // 打印去重后的元素
        .forEach(System.out::println);

        再次提醒:因为本例中使用的是String类型,所以可以直接去重。如果是自定义对象,则一定要重写hashCode和equals方法。
        翻阅该方法的源码即可发现其基于hashset进行去重,所以必须重写上述两方法。

        直接ctrl+B/左键会跳转到接口中的方法,该方法没有方法体。如果想跳过接口直接看方法体按ctrl+alt+B/左键该方法即可

合并concat

        在合并时我们需尽可能的保持两流中的数据类型保持一致,如果不同,大流中的数据类型会变为两数据类型共同的父类。相当于一次类型提升,这样就无法使用子类中的特有功能了。

        使用该方法只需将两流作为参数传入即可:

// 创建第一个ArrayList并添加初始数据
ArrayList<String> list1 = new ArrayList<>();
Collections.addAll(list1, “一”, “二”, “三”);

// 创建第二个ArrayList并添加初始数据
ArrayList<String> list2 = new ArrayList<>();
Collections.addAll(list2, “1”, “2”, “3”);

// 使用Stream.concat合并两个流
Stream.concat(list1.stream(), list2.stream())
// 打印合并后的流中的所有元素
.forEach(System.out::println);
//输出结果一 二 三 1 2 3

转换map

        因为使用过程较复杂,所以该方法仍从匿名内部类开始编写代码,然后再变为lambda表达式:现有一集合: 
Collections.addAll(list, "张无忌-11", "周芷若-22", "赵敏-33", "张强-44", "张三丰-55","张翠山-66");
        要求只获取字符串中的数字,并转为int类型。

        我们可以使用map()操作将字符串转换为int。可以使用匿名类实现Function<String, Integer>接口,其中有两个泛型,前者是流中原本的数据类型,第二个则是要转成的数据类型。因为泛型中不能写基本数据类型,所以我们使用Integer代替int。
        然后使用apply()方法定义转换逻辑:用split("-")按"-"分割字符串得到数组(例如:"张三-25" → ["张三", "25"]),再取数组第二个元素arr[1](数字部分)用Integer.parseInt()将字符串转为整数。apply()的返回类型也需是Integer,与上文中的第二个泛型保持一致。

    // 使用Stream流对集合进行操作
    list.stream()
        // 使用map操作将字符串转换为整数年龄
        .map(new Function<String, Integer>() {
            @Override
            public Integer apply(String s) {
                // 按照分隔符"-"分割字符串
                String[] arr = s.split("-");
                // 获取年龄部分(数组的第二个元素)
                String ageString = arr[1];
                // 将字符串年龄转换为整数
                int age = Integer.parseInt(ageString);
                // 返回转换后的年龄
                return age;
            }
        })
        // 打印转换后的年龄
        .forEach(System.out::println);

        然后再变为lambda表达式的形式:

// 遍历list中的每个元素,使用流处理
list.stream()
    // 将每个字符串元素转换为整数,具体操作是:
    // 1. 按"-"分割字符串
    // 2. 取分割后的第二个元素(索引为1)
    // 3. 将该字符串转换为整数
    .map(s -> Integer.parseInt(s.split("-")[1]))
    // 将转换后的每个整数打印到控制台
    .forEach(System.out::println);

终结方法

名称 说明
void forEach(Consumer action) 遍历
long count() 统计
toArray() 收集流中的数据,放到数组中
collect(Collector collector) 收集流中的数据,放到集合中

遍历forEach()

        首先是上文经常用到的forEach()方法,其形参是Consumer,是一个由@FunctionalInterface修饰的函数式接口。该接口的泛型就是流中的数据类型。
        该接口中的方法accept(String s)其形参s依次表示流中的每一个数据,所以该形参的类型也需与上文保持一致。在该方法的方法体中我们可以编写对每一个数据的处理操作。

// 使用 stream() 将 List 转换为流,然后通过 forEach 方法遍历流中的每个元素
list.stream()
        // 创建一个 Consumer 接口的匿名实现类,用于定义对每个元素的操作
        .forEach(new Consumer<String>() {
            @Override
            // accept 方法接收流中的每个元素,这里实现为打印元素到控制台
            public void accept(String s) {
                System.out.println(s);
            }
        });

        因为其是终结方法,所以其返回值为void,也就是说该方法结束后不能再调用其他方法了,其是流中的最后一步。
        接下来将其改为lambda表达式的形式,其实就是上文我们使用的样式:

list.stream().forEach(System.out::println);

统计count()

        该方法的返回类型是一个long型的整数,表示流中有多少个元素,所以在该方法后也不能再调用其他方法了:

// 使用 stream() 将 List 转换为流,然后调用 count() 方法统计元素数量
long count = list.stream().count();
System.out.println(count);

收集toArray()

         该方法有两个重载版本,一个为无参,表示收集到Object类型的数组中:

// 使用 stream() 将 List 转换为流,然后调用 toArray() 方法将流转换为数组
Object[] array = list.stream().toArray();
// 使用 Arrays.toString() 将数组转换为字符串形式,并打印到控制台
System.out.println(Arrays.toString(array));

        另一个版本的形参也是一个由@FunctionalInterface修饰的函数式接口IntFunction,其泛型表示要收集到什么类型的数组中,本文为String[]。
        其中的方法apply()形参为流中的数据的个数,该个数要和流中的数据长度保持一致。该方法的返回类型就是具体类型的数组,同样需与上文保持一致。而方法体则负责创建数组:

    // 使用 stream() 将 List 转换为流,然后通过 toArray() 方法将流转换为 String 数组
    // 这里使用 IntFunction<String[]> 接口动态创建指定长度的 String 数组
    String[] array = list.stream().toArray(new IntFunction<String[]>() {
        @Override
        // apply 方法接收流中元素的数量(value),并返回一个长度为 value 的 String 数组
        public String[] apply(int value) {
            return new String[value];
        }
    });
    
    // 使用 Arrays.toString() 将数组转换为字符串形式并打印
    System.out.println(Arrays.toString(array));
    
    // 直接打印数组对象,会输出数组的内存地址
    System.out.println(array);

        toArray方法的作用就是负责创建一个指定类型的数组,其底层会依次得到流里面的每一个数据,并把数据放到数组当中。返回值则是一个装着流里面所有数据的数组。

        接下来将其修改为lambda表达式的形式,首先来回忆lambda表达式的基础形式:()->{},其中()内为形参,若只有一个形参,则可省略()和数据类型,也就是说直接写value,表示流中元素的个数。{}同理,因为只有一句:创建数组new String[value],所以可以省略{}:

String[] array = list.stream().toArray(value -> new String[value]);
// 使用 Arrays.toString() 将数组转换为字符串形式,并打印到控制台
System.out.println(Arrays.toString(array));

        这就是收集到数组中的方法,接下来来看收集到集合中的方法。 

收集collect()

        collect()方法可以收集到单列集合List、set以及双列集合Map中,我们依次来学习。

        这里已经事先准备好了数据,要求收集所有性别为男的数据,和上文一样,我们同样可以使用s.split("-")[1]来与"男"进行比较来判断是否相等。

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌-男-15", "周芷若-女-14", "赵敏-女-13", "张强-男-20", "张三丰-男-100", "张翠山-男-40", "张良-男-35", "王二麻子-男-37", "谢广坤-男-41");
Stream<String> stream = list.stream()
                .filter(s -> "男".equals(s.split("-")[1]));

        在进行比较时,我们通常使用固定数据调用equals()方法与其他数据进行比较,因为固定数据不可能为null,而其他数据有可能为null,一旦为null则会导致空指针异常。

        进行判断后需要调用collect()方法将其收集起来,在该方法中需要传递Collectors.toList(),其中Collectors为stream流中的工具类,toList()则为其中的静态方法,其可以创建一个ArrayList集合。最后新建一个List用于接收即可:

// 使用 stream() 将 List 转换为流,然后通过 filter 方法筛选符合条件的元素
List<String> newList = list.stream()
        // 筛选条件:使用 split("-") 分割字符串,并检查第二部分是否为 "男"
        .filter(s -> "男".equals(s.split("-")[1]))
        // 将筛选后的流收集为新的 List
        .collect(Collectors.toList());
// 打印筛选后的结果
System.out.println(newList);

         收集到set集合中同理,只需将Collectors.toList()替换为Collectors.toSet(),并用Set集合接收即可:

        Set<String> newList2 = list.stream()
                .filter(s -> "男".equals(s.split("-")[1]))
                .collect(Collectors.toSet());

        两种收集方法极为相似,但收集到List中的元素会有重复,但收集到Set中的元素则不会重复。 

        接下来看如何收集到Map集合中,我们首先要确定谁作为键,谁作为值。因为题目中要求只收集男性,所以性别全部都为男不需要记录,所以我们将名字作为键,年龄作为值。

        同理,collect()中的参数替换为Collectors.toMap(),而toMap()方法中也需要传入两参,两者都为@FunctionalInterface修饰的函数式接口Function(),参数一表示键的生成规则,参数二表示值的生成规则。
        Function()有两个泛型,第一个泛型为流中数据的类型,第二个则为map键的数据类型。同理,第二个参也是Function(),第一个泛型为流中数据的类型,第二个则为map值的数据类型。
        其中的方法apply()中的形参为流中的每一个数据,其方法体则用于生成键/值,返回值就是已经生成的键/值。

        // 使用 stream() 将 List 转换为流,筛选性别为 “男” 的元素,并转换为 Map
        Map<String, Integer> map = list.stream()
                // 筛选条件:分割字符串并检查性别部分是否为 “男”
                .filter(s -> "男".equals(s.split("-")[1]))
                // 使用 Collectors.toMap 将流转换为 Map
                .collect(Collectors.toMap(
                        // 第一个 Function 提取键(姓名部分)
                        new Function<String, String>() {
                            @Override
                            public String apply(String s) {
                                return s.split("-")[0]; // 返回姓名部分作为键
                            }
                        },
                        // 第二个 Function 提取值(年龄部分)
                        new Function<String, Integer>() {
                            @Override
                            public Integer apply(String s) {
                                return Integer.parseInt(s.split("-")[2]); // 返回年龄部分作为值
                            }
                        }
                ));
        // 打印转换后的 Map
        System.out.println(map);

        接下来写成lambda表达式的形式: 

// 使用 stream() 将 List 转换为流,筛选性别为 "男" 的元素,并转换为 Map
Map<String, Integer> map = list.stream()
        // 筛选条件:分割字符串并检查性别部分是否为 "男"
        .filter(s -> "男".equals(s.split("-")[1]))
        // 使用 Collectors.toMap 将流转换为 Map
        // 第一个 Function 提取键(姓名部分)
        .collect(Collectors.toMap(
                s -> s.split("-")[0],  // 键:姓名(s.split("-")[0])
                s -> Integer.parseInt(s.split("-")[2])  // 值:年龄(s.split("-")[2])
        ));
// 打印转换后的 Map
System.out.println(map);

练习

        定义一个集合,并添加整数:1、2、3、4、5、6、7、8、9,要求过滤奇数,保留偶数并将结果保存起来。

        // 创建一个ArrayList集合并添加初始数据
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        // 使用 stream() 将 List 转换为流,通过 filter 方法筛选偶数
        List<Integer> numList = list.stream()
                // 筛选条件:保留能被 2 整除的数(偶数)
                .filter(num -> num % 2 == 0)
                // 将筛选后的流收集为新的 List
                .collect(Collectors.toList());
        // 打印筛选后的偶数列表
        System.out.println(numList);

        创建一个ArrayList集合,并添加以下字符串,字符串中前面是姓名,后面是年龄"zhangsan,23","lisi,24","wangwu,25"保留年龄大于等于24岁的人,并将结果收集到Map集合中,姓名为键,年龄为值。

        // 创建一个ArrayList集合并添加初始数据
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "zhangsan,23", "lisi,24", "wangwu,25");
        
        // 使用 stream() 将 List 转换为流,通过 filter 方法筛选年龄大于等于24岁的人
        Map<String, Integer> map = list.stream()
                // 筛选条件:保留年龄大于等于24岁的人
                .filter(s -> Integer.parseInt(s.split(",")[1]) >= 24)
                // 将筛选后的流收集为 Map 集合,姓名为键,年龄为值
                .collect(Collectors.toMap(
                        s -> s.split(",")[0], // 提取姓名作为键
                        s -> Integer.parseInt(s.split(",")[1]) // 提取年龄作为值
                ));
        
        // 打印筛选后的 Map 集合
        System.out.println(map);

        现在有两个ArrayList集合,第一个集合中:存储6名男演员的名字和年龄。第二个集合中:存储6名女演员的名字和年龄。姓名和年龄中间用逗号隔开:
"刘德华,62", "张学友,61", "郭富城,60", "黎明,59", "周星驰,63", "成龙,68"
"杨颖,38", "杨幂,38", "杨超越,24", "杨千嬅,53", "杨采妮,49", "刘亦菲,39"

要求完成如下的操作:
1,男演员只要名字为3个字的前两人
2,女演员只要姓杨的,并且不要第一个
3,把过滤后的两流合并到一起
4,将上一步的演员信息封装成Actor对象。
5,将所有的演员对象都保存到List集合中。
备注:演员类Actor,属性有:name,age

public class Main {
    public static void main(String[] args) {
        // 创建男演员和女演员的ArrayList集合
        ArrayList<String> maleActors = new ArrayList<>();
        ArrayList<String> femaleActors = new ArrayList<>();
        // 使用Collections.addAll方法向男演员集合中添加初始数据
        Collections.addAll(maleActors,
                "刘德华,62", "张学友,61", "郭富城,60", "黎明,59", "周星驰,63", "成龙,68");
        // 使用Collections.addAll方法向女演员集合中添加初始数据
        Collections.addAll(femaleActors,
                "杨颖,38", "杨幂,38", "杨超越,24", "杨千嬅,53", "杨采妮,49", "刘亦菲,39");
        
        // 使用Stream API过滤男演员:筛选名字为3个字的前两人
        Stream<String> stream1 = maleActors.stream()
                .filter(s -> s.split(",")[0].length() == 3)  // 筛选名字长度为3的演员
                .limit(2);  // 只取前两人
        
        // 使用Stream API过滤女演员:筛选姓杨的演员并跳过第一个
        Stream<String> stream2 = femaleActors.stream()
                .filter(s -> s.startsWith("杨"))  // 筛选姓杨的演员
                .skip(1);  // 跳过第一个符合条件的演员
        
        // 合并两个流,并将结果转换为Actor对象列表
        List<Actor> newList = Stream.concat(stream1, stream2)  // 合并两个流
                .map(s -> new Actor(  // 将每个字符串转换为Actor对象
                        s.split(",")[0],  // 提取名字
                        Integer.parseInt(s.split(",")[1])  // 提取年龄并转换为整数
                ))
                .collect(Collectors.toList());  // 收集为List集合
        
        // 打印最终结果
        System.out.println(newList);
    }
}

/**
 * 演员类,包含姓名和年龄属性
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
class Actor {
    private String name;  // 演员姓名
    private Integer age;  // 演员年龄
}
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐