Java Stream必知必会


铿然架构 | 作者 / 铿然一叶 这是 铿然架构 的第 119 篇原创文章


1. 前言

本文介绍Java Stream的核心应用场景、基础概念、技术优势,以及缺点。此外,还提供了实用的函数示例,帮助读者快速掌握Stream的常用操作,如filter、map、reduce等,可以作为一个入门指南,也可以作为一本实用的速查手册。

2. 常见应用场景

2.1 集合操作

使用Stream API对集合进行各种操作,如筛选、映射、过滤、排序等,使得对集合的处理更为简洁和灵活。

java 复制代码
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        List<String> result = names.stream()
                .filter(name -> name.length() > 3)
                .map(String::toUpperCase)
                .collect(Collectors.toList());

2.2 数据筛选和过滤

通过Stream的 filter 操作来筛选满足特定条件的元素。

java 复制代码
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        List<Integer> evenNumbers = numbers.stream()
                .filter(n -> n % 2 == 0)
                .collect(Collectors.toList());

2.3 数据转换和映射

使用Stream的 map 操作将集合中的元素转换为另一种类型。

java 复制代码
        List<String> words = Arrays.asList("Java", "Stream", "API");
        List<Integer> wordLengths = words.stream()
                .map(String::length)
                .collect(Collectors.toList());

2.4 数据排序

使用Stream的 sorted 操作对集合中的元素进行排序。

java 复制代码
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        List<String> sortedNames = names.stream()
                .sorted()
                .collect(Collectors.toList());

2.5 统计和汇总

使用Stream的 summarizingInt、sum 等操作进行数据统计。

java 复制代码
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        IntSummaryStatistics stats = numbers.stream()
                .collect(Collectors.summarizingInt(Integer::intValue));
        System.out.println("Sum: " + stats.getSum());
        System.out.println("Average: " + stats.getAverage());

2.6 分组和分区

使用Stream的 groupingBy 和 partitioningBy 操作进行分组和分区。

java 复制代码
        List<User> users = generateListTestData();
        Map<Integer, List<User>> userByAgeMap = users.stream()
                .collect(Collectors.groupingBy(User::getAge));
        
        Map<Boolean, List<User>> partitionedMap = users.stream()
                .collect(Collectors.partitioningBy(p -> p.getAge() > 18));

2.7 并行处理

Stream API支持并行处理,可以通过 parallel() 方法将串行Stream转换为并行Stream,提高处理效率。

java 复制代码
        List<User> users = generateListTestData();
        users.stream().parallel()
                .filter(user -> user.getSalary() > 5000)
                .map(user -> user.getName())
                .forEach(name -> {
                    System.out.println(name);
                });

3. 优点和缺点

3.1 优点

Stream API提供了一种声明性的编程风格,相比于传统的迭代方式,Stream API更注重描述你想要做什么,而非怎么做。

其优点如下:

● 函数式编程风格

Stream API支持函数式编程,提供了一种更为简洁、易读的代码风格,使得代码更具表达性。

● 链式调用

可以通过链式调用一系列的操作,形成一个流水线,减少了中间变量的使用,代码更紧凑。

● 延迟执行

Stream 具有延迟执行特性,只有在终止操作被调用时才会执行中间操作。这可以提高效率,只计算实际需要的结果。

● 并行处理

Stream API 内建支持并行处理,可以充分利用多核处理器,提高处理大数据集的性能。

● 更好的抽象

Stream 提供了更高层次、更抽象的操作,使得代码更容易理解和维护。

● 内置丰富的操作

Stream API 提供了丰富的操作,如过滤、映射、归约等,减少了对集合的频繁迭代,提高了代码的可读性和可维护性。

3.2 缺点

● 学习曲线

对于初学者来说,学习使用Stream API可能需要一些时间,特别是对于那些不熟悉函数式编程的开发者。

● 性能问题

在某些情况下,Stream并不一定比传统的循环方式性能更好。而且在并行处理时,需要谨慎处理共享变量等线程安全问题。

● 不适用于所有场景

并不是所有的业务逻辑都适合使用 Stream API,有些简单的操作可能使用传统的方式更为直观。

● 不可重用

一旦Stream被消费(即执行了终止操作),它就不能再次被使用。如果需要再次操作相同的数据集,需要重新创建一个新的Stream。

● 难以调试

由于Stream是延迟执行的,当出现问题时,可能需要花费更多的时间来定位问题,不如传统的迭代方式易于调试。

4. 基础概念和要点

4.1 中间操作和终止操作

4.1.1 定义

Stream的操作分为中间操作和终止操作。

中间操作返回一个新的Stream,并且是惰性求值的,只有在遇到终止操作时才会执行。

终止操作会将流标记为已消耗,之后就不能再使用它。

一个Stream管道可以有多个中间操作,但只能有一个终止操作,如果执行多个终止操作会抛出类似如下异常:

java 复制代码
java.lang.IllegalStateException: stream has already been operated upon or closed

	at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
	at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
	at com.kengcoder.javastreamawsome.StreamTest.foreachCalledMultipleTimes(StreamTest.java:131)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

4.1.2 终止操作列表

Java Stream的终止操作是对流进行最终处理并生成最终结果的操作。这些操作会触发流的遍历,并且只能在流上执行一次。以下是一些常见的Java Stream终止操作:

● forEach

对流中的每个元素执行指定的操作,可以用于迭代流中的元素。

● toArray

将流中的元素转换为数组。

● collect

将流中的元素收集到一个集合或者其他数据结构中,Collectors类提供了一系列静态工厂方法,用于创建常见的收集器。

● reduce

通过指定的二进制操作,将流中的元素合并为一个值,通常用于对流中的元素进行累积操作。

● count

返回流中元素的个数。

● min和max

返回流中的最小或最大元素,需要传递一个比较器(Comparator)。

● findFirst和findAny

返回流中的第一个元素或者任意一个元素,在并行流中,findAny可能会更有效率。

4.2 惰性求值

4.2.1 定义

惰性求值(Lazy Evaluation)指的是当使用Stream API时,并不是立即对数据进行处理,只有在需要结果的时候才会执行。这种方式可以提高性能,尤其是在处理大量数据时。

惰性求值主要体现在中间操作(Intermediate Operations)上,中间操作,如filter, map, sorted等,都是惰性的,它们只在终止操作(Terminal Operation)执行时才真正处理数据,如collect, forEach, reduce, 会触发实际的数据处理。

4.2.2 例子

数据:

java 复制代码
        User[] users = new User[] {
                new User("John", 18, 7000),
                new User("Jak", 25, 9000),
                new User("leo", 22, 8000)
        };

代码:

java 复制代码
    public void lazyEvaluation() {
        // 准备演示数据
        List<User> users = generateListTestData();
        List<String> userNames = new ArrayList<>();
        users.stream().forEach(user-> {
            userRepository.put(user.getName(), user);
            userNames.add(user.getName());
        });

        User user = userNames.stream()
                .map(name -> {
                    System.out.println("Mapping name: " + name);
                    return findUserByName(name);
                })
                .filter(u -> {
                    System.out.println("Filtering name: " + u.getName());
                    return u.getSalary() > 7000;
                })
                .findFirst()
                .orElse(null);
        System.out.println(user.toString());
    }

    private static User findUserByName(String name) {
        return userRepository.get(name);
    }

上面代码并不会因为Stream有3条记录,先执行3次map操作得到结果后通过filter过滤数据,其执行逻辑如下:

● filter和map是中间操作,它们定义了如何处理流中的元素,但不会立即执行。

● findFirst是终止操作,它触发了实际的处理。

● 由于findFirst只需要找到第一个匹配的元素,流的处理会在找到第一个匹配的元素后立即停止。这就是惰性求值的好处:不需要处理整个流,只处理必要的部分。

输出:

java 复制代码
Mapping name: John
Filtering name: John
Mapping name: Jak
Filtering name: Jak
User{name='Jak', age=25, salary=9000}

4.3 并行操作

使用parallel操作可以在处理大量数据时充分利用多核处理器的性能,但要关注一些注意事项和陷阱。以下是使用parallel操作时的注意事项:

● 线程安全性

并行流的操作可能会在多个线程中并发执行,因此要确保操作是线程安全的。如果在并行操作中修改了共享的可变状态,可能会导致竞态条件和不确定的结果。

● 不适用于所有场景

并行流对于某些类型的操作和数据集合并不适用,因为并行化可能会引入额外的开销,导致性能下降。在小型数据集或简单操作的情况下,使用并行流可能不会带来明显的性能优势。

● 保持并行性性能

并行流的性能取决于可用的处理器核心数量和任务的划分。在某些情况下,流的划分和合并可能会导致性能下降,要保持并行性性能,可以调整线程池的大小或使用更细粒度的任务划分。

● 避免副作用

并行流应该避免副作用,副作用是指在流操作中改变了共享状态,可能导致难以预测的结果。

● 终止操作的选择

不同的终止操作可能对性能产生不同的影响。例如,forEach终止操作不保证元素的顺序,而forEachOrdered会保持元素的顺序。选择适当的终止操作以满足需求。

● 资源管理

并行流可能会创建多个线程,因此需要注意资源管理。确保及时关闭流(指使用的文件流,网络流),以释放资源。

4.4 无线流

无限流(Infinite Stream)是一种特殊类型的流,它包含无限数量的元素。与有限流不同,无限流没有明确定义的末尾,因此可以无限地生成元素。无限流通常用于表示潜在的无限数据源或表示无限序列。

可以使用Stream.generate创建无限流: Stream.generate方法有一个 Supplier(供应者)参数,它可以生成流的元素,由于供应者可以无限生成元素,因此可以创建无限流。

例子:

java 复制代码
Stream<Integer> infiniteStream = Stream.generate(() -> 1);

上述示例创建了一个包含无限数量的整数1的流。

无限流要谨慎使用,如果不适当限制或控制,会导致无限循环或内存耗尽。因此,在使用无限流时,通常需要使用中间操作来限制流的大小或在终止操作中设置终止条件,例如,使用limit来限制无限流的大小,或使用findFirst 来获取满足某个条件的第一个元素。

例子:

java 复制代码
Stream<Integer> finiteStream = infiniteStream.limit(10); // 限制为前10个元素

4.5 占用空间

4.5.1 误解

Java Stream每个方法都会返回Stream实例,导致很多人以为操作步骤多了之后,会有很多Stream生成,造成占用更多的内存。

例如:

java 复制代码
Employee employee = Stream.of(empIds)
      .map(employeeRepository::findById)
      .filter(e -> e != null)
      .filter(e -> e.getSalary() > 100000)
      .findFirst()
      .orElse(null);

4.5.2 正解

Java Stream不会占用很大的内存空间,它们是一种用于处理集合数据的高级抽象。关于是否占用内存,可从下面两方面来参考:

● 中间操作和终止操作

Stream的操作分为中间操作和终止操作。中间操作返回一个新的Stream,并且是惰性求值,只有在遇到终止操作时才会执行。如果不注意,可能会在中间操作上执行过多的计算,导致占用更多内存。

java 复制代码
List<String> list = Arrays.asList("a", "b", "c");
List<String> result = list.stream()
                        .map(s -> s.toUpperCase())
                        .collect(Collectors.toList());

上面的例子中,map 操作返回一个新的Stream,但并没有改变原始列表。只有在collect方法调用时,才会生成新的列表。如果操作链很大,可能会占用较多内存。

● 无限流

Stream可以表示无限的数据集,例如Stream.iterate和 Stream.generate,如果你在无限流上使用不当的中间操作或者不加终止条件,就有可能导致无限循环,最终占用大量内存。

java 复制代码
Stream<Integer> infiniteStream = Stream.iterate(0, i -> i + 1);
List<Integer> result = infiniteStream.limit(10)
                                  .collect(Collectors.toList());

在这个例子中,limit(10) 添加了一个终止条件,限制了生成的元素数量。

总的来说,Java Stream本身并不会占用大量内存。如果在使用过程中出现了内存问题,建议仔细检查中间操作和终止操作的使用,确保操作链是合理的、并且不会无限增长。

4.6 Stream和IntStream的区别

Stream和IntStream都是Java 8中引入的流(Stream)类型,它们主要区别在于处理的数据类型和用途。

4.6.1 Stream

Stream 表示一个包含整数对象的流,可以包含任何整数的包装类型Integer,如Integer、Long、Double等。

它是一个泛型流,适用于处理任何引用类型的元素,而不仅仅是基本数据类型。

Stream 适用于处理复杂对象、集合或其他引用类型的元素。

示例:

java 复制代码
Stream<Integer> integerStream = Arrays.stream(new Integer[]{1, 2, 3, 4, 5});

4.6.2 IntStream

IntStream是一种专门用于处理原始整数数据的流。它是Java中的原始数据类型流,只能包含基本数据类型int的元素。

由于IntStream专门针对整数进行了优化,因此在某些情况下可以提供更好的性能。

IntStream提供了许多专门用于处理整数的方法,如sum()、average()、max()、min()等。

示例:

java 复制代码
IntStream intStream = IntStream.of(1, 2, 3, 4, 5);

4.6.3 小结

Stream是一个通用的流,适用于处理包装类型的对象,它不限于处理整数。

IntStream 是一个专门用于处理原始整数数据的流,它提供了更多的整数特定方法,并且在某些情况下可以提供更好的性能。

可以根据具体的需求选择使用哪种类型的流。如果需要处理整数对象,使用Stream;如果需要处理原始整数,使用IntStream。

5. 使用方法参考

5.1 创建stream

5.1.1 Stream

有如下三方方式创建:

java 复制代码
        User[] users = new User[] {
                new User("John", 18),
                new User("Jak", 25)
        };

        // 方式一:通过数组得到
        Stream stream = Stream.of(users);

        // 方式二:通过集合得到
        List<User> userList = Arrays.asList(users);
        userList.stream();

        // 方式三:通过Builder创建,应该用得很少
        Stream.Builder<User> userStreamBuilder = Stream.builder();
        userStreamBuilder.accept(users[0]);
        userStreamBuilder.accept(users[1]);
        Stream<User> empStream = userStreamBuilder.build();

5.1.2 IntStream

java 复制代码
        IntStream intStream = IntStream.of(1, 2, 3);

        // 创建指定区间的int流,左闭右开
        IntStream rangeIntStream = IntStream.range(10, 15);
        rangeIntStream.forEach(System.out::println);

输出:

java 复制代码
10
11
12
13
14

5.2 方法

5.2.0 使用的验证数据

java 复制代码
    private User[] generateArrayTestData() {
        User[] users = new User[] {
                new User("John", 18, 7000),
                new User("Jak", 25, 9000),
                new User("leo", 22, 8000)
        };
        return users;
    }

    private List<User> generateListTestData() {
        User[] users = generateArrayTestData();
        List<User> list = new ArrayList<User>();
        list.addAll(Arrays.asList(users));
        return list;
    }

5.2.1 遍历

5.2.1.1 单次遍历-foreach

foreach是终止操作,在一个流上只能操作一次。

5.2.1.1.1 正确用法

代码:

java 复制代码
    public void foreach() {
        List<User> users = generateListTestData();
        users.stream().forEach(System.out::println);

        // 可以操作流里的对象,操作将生效
        users.stream().forEach(user-> user.setSalary(user.getSalary() + 200));

        System.out.println("\n*********** 工资增加后 ************");
        // 再次打印,余额都增加了200
        users.stream().forEach(System.out::println);
    }

输出:

java 复制代码
User{name='John', age=18, salary=7000}
User{name='Jak', age=25, salary=9000}
User{name='leo', age=22, salary=8000}

*********** 工资增加后 ************
User{name='John', age=18, salary=7200}
User{name='Jak', age=25, salary=9200}
User{name='leo', age=22, salary=8200}
5.2.1.1.2 错误用法

终止操作不能多次调用,否则会抛出异常。

代码:

java 复制代码
    public void foreachCalledMultipleTimes() {
        List<User> users = generateListTestData();
        Stream<User> userStream = users.stream();
        userStream.forEach(System.out::println);
        userStream.forEach(System.out::println);
    }

输出:

java 复制代码
User{name='John', age=18, salary=7000}
User{name='Jak', age=25, salary=8000}
User{name='leo', age=22, salary=8000}

java.lang.IllegalStateException: stream has already been operated upon or closed

	at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
	at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
	at com.kengcoder.javastreamawsome.StreamTest.foreachCalledMultipleTimes(StreamTest.java:131)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)

5.2.1.2 多次遍历-peek

peek和foreach的区别是:peek是中间操作,因此可以多次调用,而foreach是终止操作,只能调用一次。

代码:

java 复制代码
    public void peek() {
        List<User> users = generateListTestData();

        System.out.println("**********  原始数据 打印输出 **************");
        users.stream().forEach(System.out::println);

        System.out.println("\n**********  peek 打印输出 **************");
        // peek可以多次遍历操作
        List<User> resultList = users.stream()
                .peek(user-> user.setSalary(user.getSalary() + 200))
                .peek(System.out::println)
                .collect(Collectors.toList());

        System.out.println("\n**********  最终结果 打印输出 **************");
        resultList.stream().forEach(System.out::println);
    }

这段代码有几个要点:

1.peek(System.out::println)这里并不会触发打印,因为peek是一个中间操作,需要有后面的终止操作collect才会实际打印,感兴趣的可以去掉试试。

2.中间peek(System.out::println)做了打印输出,并不会影响最后通过collect得到一个全量集合,也就是在多步操作中可以通过peek做任何事情。

输出:

java 复制代码
**********  原始数据 打印输出 **************
User{name='John', age=18, salary=7000}
User{name='Jak', age=25, salary=8000}
User{name='leo', age=22, salary=8000}

**********  peek 打印输出 **************
User{name='John', age=18, salary=7200}
User{name='Jak', age=25, salary=8200}
User{name='leo', age=22, salary=8200}

**********  最终结果 打印输出 **************
User{name='John', age=18, salary=7200}
User{name='Jak', age=25, salary=8200}
User{name='leo', age=22, salary=8200}

5.2.2 转换

5.2.2.1 转换对象-map

遍历集合,调用一个函数,函数参数来自集合成员,函数结果是转换后的对象,例如根据用户名称列表查询用户。

java 复制代码
    public void map() {
        // 准备演示数据
        List<User> users = generateListTestData();
        List<String> userNames = new ArrayList<>();
        users.stream().forEach(user-> {
            userRepository.put(user.getName(), user);
            userNames.add(user.getName());
        });


        // 遍历userNames,并将集合元素作为参数传给StreamExample::findUserByName方法,将函数返回结果生成1个新的stream
        Stream<User> newUserStream = userNames.stream().map(StreamTest::findUserByName);
        List<User> resultList = newUserStream.collect(Collectors.toList());
        resultList.stream().forEach(System.out::println);

输出:

java 复制代码
User{name='John', age=18, salary=7000}
User{name='Jak', age=25, salary=9000}
User{name='leo', age=22, salary=8000}

注意:map方法需要保证stream里的每个成员都能返回对象,不能为空,否则会报空指针异常,如果可能为空,可以考虑返回Optional。

如下代码:

java 复制代码
        Stream<User> newUserStream = userNames.stream().map(StreamExample::findUserByName);
        List<User> resultList = newUserStream.collect(Collectors.toList());

等同:

java 复制代码
        List<User> userList = new ArrayList<>();
        for(String userName: userNames) {
            User user = StreamExample.findUserByName(userName);
            userList.add(user);
        }

可以看到stream方式简化很多。

5.2.2.2 将stream转换为List-collect

实际前面已经演示:

java 复制代码
        Stream<User> newUserStream = userNames.stream().map(StreamExample::findUserByName);
        List<User> resultList = newUserStream.collect(Collectors.toList());

5.2.2.3 转换为Set-toSet

将Stream转换为Set。

代码:

java 复制代码
    public void toSet() {
        List<User> users = generateListTestData();
        Set<String> result = users.stream().map(User::getName).collect(Collectors.toSet());
        System.out.println(result);
    }

输出:

csharp 复制代码
[Jak, leo, John]

5.2.2.4 stream转换为数组-toArray

将stream转换为数组。

代码:

java 复制代码
    public void toArray() {
        List<User> users = generateListTestData();
        User[] userArr = users.stream().toArray(User[]::new);
        Stream.of(userArr).forEach(System.out::println);
    }

5.2.2.5 转换为IntStream-mapToInt

将Stream转换为IntStream,处理的对象需要是int类型。

代码:

java 复制代码
    public void mapToInt() {
        List<User> users = generateListTestData();
        users.stream().mapToInt(User::getSalary).forEach(System.out::println);
    }

输出:

java 复制代码
7000
9000
8000

5.2.3 过滤、查找和匹配

5.2.3.1 过滤数据-filter

根据条件过滤stream里的数据,只返回满足条件的数据。

代码:

java 复制代码
    public void filter() {
        List<User> users = generateListTestData();
        List<User> resultUsers = users.stream().filter(user -> user.getSalary() > 7000 && user.getAge() < 25).collect(Collectors.toList());

        users.stream().forEach(user -> System.out.println(user.toString()));
        System.out.println("***** result (user.getSalary() > 7000 && user.getAge() < 25) ******");
        resultUsers.stream().forEach(System.out::println);
    }

输出:

java 复制代码
User{name='John', age=18, salary=7000}
User{name='Jak', age=25, salary=8000}
User{name='leo', age=22, salary=8000}
***** result (user.getSalary() > 7000 && user.getAge() < 25) ******
User{name='leo', age=22, salary=8000}

5.2.3.2 去重复数据-distinct

去掉Stream中重复数据,注意:java对象去重要正确实现equals方法和hashCode方法,否则可能得到错误结果。

代码:

java 复制代码
    public void distinct() {
        List<User> users = generateListTestData();
        // 添加一个相同用户
        users.add(new User("leo", 22, 8000));

        System.out.println("*************  init data ******************");
        users.stream().forEach(System.out::println);

        System.out.println("\n*************  result data ******************");
        users.stream().distinct().forEach(System.out::println);
    }

输出:

java 复制代码
*************  init data ******************
User{name='John', age=18, salary=7000}
User{name='Jak', age=25, salary=9000}
User{name='leo', age=22, salary=8000}
User{name='leo', age=22, salary=8000}

*************  result data ******************
User{name='John', age=18, salary=7000}
User{name='Jak', age=25, salary=9000}
User{name='leo', age=22, salary=8000}

对象equal和hashCode方法实现举例:

java 复制代码
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            User user = (User) o;
            return age == user.age &&
                    salary == user.salary &&
                    Objects.equals(name, user.name);
        }

        @Override
        public int hashCode() {
            return Objects.hash(name, age, salary);
        }

5.2.3.3 查找首个成员-findFirst

查找Stream中首个成员。

代码:

java 复制代码
    public void findFirst() {
        List<User> users = generateListTestData();
        // 返回对象可能为空,用Optional来封装
        Optional<User> oUser = users.stream().findFirst();
        if (oUser.isPresent()) {
            System.out.println(oUser.get());
        }
    }

输出:

java 复制代码
User{name='John', age=18, salary=7000}

5.2.3.4 查找任意成员-findAny

查找Stream中任意一个成员,这在并行流中更有用(并行执行,谁先返回就用谁),在非并行流中和findFirst比没有啥区别。

代码:

java 复制代码
    public void findAny() {
        List<User> users = generateListTestData();
        // 返回对象可能为空,用Optional来封装
        Optional<User> oUser = users.stream().filter(user -> user.getSalary() > 7000).findAny();
        if (oUser.isPresent()) {
            System.out.println(oUser.get());
        }
    }

输出:

java 复制代码
User{name='Jak', age=25, salary=9000}

5.2.3.5 所有成员匹配条件-allMatch

Stream中所有成员匹配条件返回true,否则返回false。

代码:

java 复制代码
    public void allMatch() {
        List<User> users = generateListTestData();
        // 是否所有人工资都大于8000
        boolean result = users.stream().allMatch(user -> user.getSalary() > 8000);
        System.out.println("result: " + result);
    }

输出:

java 复制代码
result: false

5.2.3.6 任何一个成员匹配条件-anyMatch

Stream中任何一个成员匹配条件返回true,否则返回false。

代码:

java 复制代码
    public void anyMatch() {
        List<User> users = generateListTestData();
        // 是否有任何一个人工资大于8000
        boolean result = users.stream().anyMatch(user -> user.getSalary() > 8000);
        System.out.println("result: " + result);
    }

输出:

java 复制代码
result: true

5.2.3.7 没有成员匹配条件-noneMatch

Stream中没有任何一个成员匹配条件返回true,否则返回false。

代码:

java 复制代码
    public void noneMatch() {
        List<User> users = generateListTestData();
        // 是否没有任何一个人工资大于8000
        boolean result = users.stream().noneMatch(user -> user.getSalary() > 8000);
        System.out.println("result: " + result);
    }

输出:

java 复制代码
result: false

5.2.4 排序、分区和分组

5.2.4.1 排序-sorted

根据排序规则将Stream成员排序。

代码:

java 复制代码
    public void sorted() {
        List<User> users = generateListTestData();
        // 按用户年龄升序排列
        users.stream().sorted((u1, u2) -> u1.getAge() < u2.getAge() ? -1 : 1).forEach(System.out::println);
    }

输出:

java 复制代码
User{name='John', age=18, salary=7000}
User{name='leo', age=22, salary=8000}
User{name='Jak', age=25, salary=9000}

5.2.4.2 数据分区-partitioningBy

将数据按照条件分成两组,一组满足条件,一组不满足条件。

代码:

java 复制代码
    public void partitioningBy() {
        List<User> users = generateListTestData();
        Map<Boolean, List<User>> userMap = users.stream().collect(Collectors.partitioningBy(user -> user.getSalary() > 8000));
        System.out.println("************ salary > 8000 ************");
        userMap.get(Boolean.TRUE).stream().forEach(System.out::println);

        System.out.println("\n************ salary <= 8000 ************");
        userMap.get(Boolean.FALSE).stream().forEach(System.out::println);
    }

输出:

markdown 复制代码
************ salary > 8000 ************
User{name='Jak', age=25, salary=9000}

************ salary <= 8000 ************
User{name='John', age=18, salary=7000}
User{name='leo', age=22, salary=8000}

5.2.4.3 数据分组-groupingBy

将数据分成多组,根据groupingBy参数的函数接口返回结果来分组,所以当返回结果是Boolean时,等同partitioningBy。

代码:

java 复制代码
    public void groupingBy() {
        List<User> users = generateListTestData();
        Map<Integer, List<User>> userMap = users.stream().collect(Collectors.groupingBy(user -> user.getSalary()));

        for (Map.Entry<Integer, List<User>> entry: userMap.entrySet()) {
            System.out.println("\nmap key: " + entry.getKey());
            System.out.println("map value: " + entry.getValue());
        }
    }

输出:

arduino 复制代码
map key: 8000
map value: [User{name='leo', age=22, salary=8000}]

map key: 9000
map value: [User{name='Jak', age=25, salary=9000}]

map key: 7000
map value: [User{name='John', age=18, salary=7000}]

5.2.4.4 对分组结果做转换-mapping

先做分组,然后将分组后的值做进一步转换,分组key始终不变。

代码:

java 复制代码
    public void mapping() {
        List<User> users = generateListTestData();
        Map<Boolean, List<String>> userMap = users.stream().collect(
                Collectors.groupingBy(
                        user -> user.getSalary() > 8000,
                        Collectors.mapping(User::getName, Collectors.toList())
                )
        );

        System.out.println("************ salary > 8000 ************");
        userMap.get(Boolean.TRUE).stream().forEach(System.out::println);

        System.out.println("\n************ salary <= 8000 ************");
        userMap.get(Boolean.FALSE).stream().forEach(System.out::println);
    }

输出:

markdown 复制代码
************ salary > 8000 ************
Jak

************ salary <= 8000 ************
John
leo

5.2.5 汇总和聚合

5.2.5.1 取最小值-min

根据规则取最小值的对象,规则可以参考对象的属性,例如年龄,身高,薪酬等。

代码:

java 复制代码
    public void min() {
        List<User> users = generateListTestData();
        User user = users.stream().min((u1, u2) -> u1.getAge() - u2.getAge()).orElse(null);
        // 比较方法和下面这个等价
        // User user = users.stream().min(Comparator.comparingInt(User::getAge)).orElse(null);
        System.out.println(user);
    }

输出:

java 复制代码
User{name='John', age=18, salary=7000}

5.2.5.2 取最大值-max

根据规则取最大值的对象,规则可以参考对象的属性,例如年龄,身高,薪酬等。

代码:

java 复制代码
    public void max() {
        List<User> users = generateListTestData();
        User user = users.stream().max(Comparator.comparing(User::getAge)).orElse(null);
        System.out.println(user);
    }

输出:

java 复制代码
User{name='Jak', age=25, salary=9000}

5.2.5.3 求平均值-average

平均值是一个数值,求平均值前要先将待求值对象转换为数字类型。

代码:

java 复制代码
    public void average() {
        List<User> users = generateListTestData();
        Double avgSalary = users.stream().mapToInt(User::getSalary).average().orElse(-1);
        System.out.println(avgSalary);
    }

输出:

yaml 复制代码
8000.0

5.2.5.4 聚合操作-reduce

reduce用于将流中的元素进行聚合操作,将它们合并为一个单一的结果。

reduce方法接受一个二元操作(BinaryOperator)作为参数,这个操作定义了如何将流中的元素组合在一起,此时结果是一个包含最终聚合结果的Optional对象,因为流可能为空。

reduce方法有多个重载形式,其中最常用的形式是接受一个初始值(identity)和一个二元操作,这个初始值是操作的起始值,而二元操作定义了如何将流中的元素逐个合并到结果中,这样结果就不会为空。

reduce方法的应用不限于求和,可以根据需要执行各种聚合操作,例如求最大值、最小值、拼接字符串等,只需根据具体情况定义合适的二元操作即可。

5.2.5.4.1 求和

获取员工工资总额。

代码:

java 复制代码
    public void reduce() {
        List<User> users = generateListTestData();

        // 求和,reduce方法没有设置初始值,结果可能为空
        Optional<Integer> optSumSalary = users.stream()
                .map(User::getSalary)
                .reduce((x, y) -> x + y);
        if (optSumSalary.isPresent()) {
            System.out.println(optSumSalary.get());
        }

        // 求和,reduce方法设置了初始值,结果不会为空
        Integer sumSalary = users.stream()
                .map(User::getSalary)
                .reduce(0, Integer::sum);
        System.out.println(sumSalary);
    }

输出:

java 复制代码
24000
24000
5.2.5.4.2 求最大值

获取员工最高工资。

代码:

java 复制代码
    public void reduceMax() {
        List<User> users = generateListTestData();

        // 求最大值,等同max
        Optional<Integer> optMaxSalary = users.stream()
                .map(User::getSalary)
                .reduce((x, y) -> x > y ? x : y);
        if (optMaxSalary.isPresent()) {
            System.out.println(optMaxSalary.get());
        }
    }

输出:

java 复制代码
9000
5.2.5.4.3 拼接

将员工姓名拼接起来。

代码:

java 复制代码
    public void reduceJoin() {
        List<User> users = generateListTestData();

        // 拼接
        Optional<String> optNameStr = users.stream()
                .map(User::getName)
                .reduce((x, y) -> x + ", " + y);
        if (optNameStr.isPresent()) {
            System.out.println(optNameStr.get());
        }
    }

输出:

java 复制代码
John, Jak, leo

5.2.5.5 增强聚合操作-reducing

reducing可以完成和reduce一样的操作,除此之外还能对分组结果做聚合操作。

总的来说,如果不是分组聚合用reduce,分组聚合用reducing

5.2.5.5.1 等同reduce

代码:

java 复制代码
    public void reducing() {
        List<User> users = generateListTestData();

        // 一般聚合,同reduce
        Integer sumSalary = users.stream().collect(Collectors.reducing(0, User::getSalary, (x, y)-> x + y));
        System.out.println("sumSalary = " + sumSalary);

        // 回顾reduce写法,比较异同,逻辑是类似的,都要先做转换(map操作),然后做聚合
        Optional<Integer> optSumSalary = users.stream()
                .map(User::getSalary)
                .reduce((x, y) -> x + y);
        if (optSumSalary.isPresent()) {
            System.out.println("\noptSumSalary = " + optSumSalary.get());
        }
    }

输出:

ini 复制代码
sumSalary = 24000

optSumSalary = 24000
5.2.5.5.2 分组求最小值

分组后工资最低的人。

代码:

java 复制代码
    public void reducingMin() {
        List<User> users = generateListTestData();

        Map<Boolean, Optional<User>> userMap = users.stream().collect(
                Collectors.groupingBy(
                        user -> user.getSalary() > 7000,
                        Collectors.reducing(BinaryOperator.minBy(Comparator.comparing(User::getSalary)))
                )
        );

        System.out.println("\n************ 工资 > 7000里最少的人 ************");
        Optional<User> optUser = userMap.get(Boolean.TRUE);
        if (optUser.isPresent()) {
            System.out.println(optUser);
        }
    }

输出:

java 复制代码
************ 工资 > 7000里最少的人 ************
Optional[User{name='leo', age=22, salary=8000}]
5.2.5.5.3 分组count

按工资分组统计。

代码:

java 复制代码
    public void reducingCount() {
        List<User> users = generateListTestData();

        Map<Boolean, Integer> userMap = users.stream().collect(
                Collectors.groupingBy(
                        user -> user.getSalary() > 7000,
                        Collectors.reducing(0, u -> 1, Integer::sum)
                )
        );

        System.out.println("\n************ 工资 > 7000的人数 ************");
        int moreThenSum = userMap.get(Boolean.TRUE);
        System.out.println(moreThenSum);

        System.out.println("\n************ 工资 <= 7000的人数 ************");
        int lessThenSum = userMap.get(Boolean.FALSE);
        System.out.println(lessThenSum);
    }

Collectors.reducing(0, u -> 1, Integer::sum)含义:

0是初始值;u是user对象,这里用不上,但是必须作为一个参数传入,对于每个user记录,这里直接返回1用于count;Integer::sum为对第2个参数返回值求和,这样最终就得到总记录数。

输出:

java 复制代码
************ 工资 > 7000的人数 ************
2

************ 工资 <= 7000的人数 ************
1

5.2.5.6 汇总统计-summarizingDouble

一次得到各个汇总值,包括count、min、max、average、sum。

代码:

java 复制代码
    public void summarizingDouble() {
        List<User> users = generateListTestData();
        DoubleSummaryStatistics stats = users.stream().collect(Collectors.summarizingDouble(User::getSalary));
        System.out.println(stats);
    }

输出:

java 复制代码
DoubleSummaryStatistics{count=3, sum=24000.000000, min=7000.000000, average=8000.000000, max=9000.000000}

5.2.5.7 汇总统计-summaryStatistics

和summarizingDouble方法结果一样,仅处理过程有些差别,summarizingDouble是在调用汇总时做类型转换,summaryStatistics是先做类型转换,然后再调用汇总方法。

代码:

java 复制代码
    public void summaryStatistics() {
        List<User> users = generateListTestData();
        DoubleSummaryStatistics stats = users.stream().mapToDouble(User::getSalary).summaryStatistics();
        System.out.println(stats);
    }

输出:

java 复制代码
DoubleSummaryStatistics{count=3, sum=24000.000000, min=7000.000000, average=8000.000000, max=9000.000000}

5.2.5.8 拼接字符串-joining

通过Collectors拼接字符串,reduce也可以实现。

代码:

java 复制代码
    public void joining() {
        List<User> users = generateListTestData();
        String result = users.stream().map(User::getName).collect(Collectors.joining(", "));
        System.out.println(result);
    }

输出:

java 复制代码
John, Jak, leo

5.3 并行操作-parallel

Stream中的操作可以并行执行。

代码:

java 复制代码
    public void parallel1() {
        List<User> users = generateListTestData();
        final long SLEEP_TIME_MILLIS = 1000;
        long startTime = System.nanoTime();
        // 这里parallel是放在最前,也可以试试放到终止操作前的效果 (对于这段代码效果一样)
        users.stream().parallel()
                .filter(user -> {
                    pause(SLEEP_TIME_MILLIS);
                    printCostTime("filtering", startTime);
                    return user.getSalary() > 5000;
                })
                .map(user -> {
                    pause(SLEEP_TIME_MILLIS);
                    printCostTime("mappping", startTime);
                    return user.getName();
                })
                .forEach(name -> {
                    pause(SLEEP_TIME_MILLIS);
                    printCostTime("forEach", startTime);
                    System.out.println(name);
                });
    }

输出:

java 复制代码
filtering: cost time: 1
filtering: cost time: 1
filtering: cost time: 1
mappping: cost time: 2
mappping: cost time: 2
mappping: cost time: 2
forEach: cost time: 3
John
forEach: cost time: 3
Jak
forEach: cost time: 3
leo

可以看到每一类操作是并行的,需要注意的是:不同类的操作之间也是并行执行,后面的某类操作并不会等前面的其他类操作执行完后再执行,例如map不会等待所有filter执行完才执行,这里看起来是要等待,是因为休眠时间都一样。

去掉parallel方法,输出:

java 复制代码
filtering: cost time: 1
mappping: cost time: 2
forEach: cost time: 3
John
filtering: cost time: 4
mappping: cost time: 5
forEach: cost time: 6
Jak
filtering: cost time: 7
mappping: cost time: 8
forEach: cost time: 9
leo

可以看到不并行的话,每一类操作要等上一类操作执行完,然后重新开始遍历。

5.4 无限流

5.4.1 生成无限流-

代码:

java 复制代码
    public void generate() {
        Stream.generate(Math::random)
                .limit(5)
                .forEach(System.out::println);
    }

注意:为了防止无限流数据过大内存溢出,要使用limit方法限制返回记录数。

输出:

java 复制代码
0.3871587847555701
0.9902689986260956
0.7646547158244698
0.9718160831187586
0.9430635626727983

5.4.2 迭代-iterate

代码:

java 复制代码
    public void iterate() {
        // 初始值是2,然后乘2,将得到的结果继续乘2,无限循环
        Stream.iterate(2, i -> i * 2)
                .limit(5)
                .forEach(System.out::println);
    }

输出:

java 复制代码
2
4
8
16
32

6. 总结

总体来说,Java Stream是一个强大而灵活的工具,可以提高代码的简洁性和可读性,特别是在处理复杂的数据操作时。然而,开发者需要权衡使用时的优缺点,并根据具体场景选择是否使用Stream API。


其他阅读:

萌新快速成长之路
如何编写软件设计文档
Spring Cache架构、机制及使用
布隆过滤器适配Spring Cache及问题与解决策略
JAVA编程思想(一)通过依赖注入增加扩展性
JAVA编程思想(二)如何面向接口编程
JAVA编程思想(三)去掉别扭的if,自注册策略模式优雅满足开闭原则
Java编程思想(七)使用组合和继承的场景
JAVA基础(一)简单、透彻理解内部类和静态内部类
JAVA基础(二)内存优化-使用Java引用做缓存
JAVA基础(三)ClassLoader实现热加载
JAVA基础(五)函数式接口-复用,解耦之利刃

相关推荐
苏三说技术9 小时前
Claude Code从失控到起飞,只用了这些技巧
后端
长栎10 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode10 小时前
Redis 在生产项目的使用
前端·后端
用户5598224812210 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode10 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战10 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha10 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn10 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户7623524259110 小时前
ShardingJDBC
后端
行者全栈架构师10 小时前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端