作为一名 Java 开发者,我还记得刚接触 Stream 流时的惊喜 —— 原来数据处理可以如此简洁优雅。对于刚学习 Java 的大学生来说,Stream 流可能看起来有点抽象,但一旦掌握,它会成为你代码中的 "瑞士军刀"。今天我们就来聊聊 Stream 流到底是什么,以及它能帮我们解决什么问题。

一、为什么需要 Stream 流?

在没有 Stream 流的年代,我们处理集合数据通常是这样的:

// 传统方式:筛选出年龄大于18的学生并打印名字
List<Student> students = ...;
for (Student s : students) {
    if (s.getAge() > 18) {
        System.out.println(s.getName());
    }
}

这种代码没什么问题,但当需求变得复杂时(比如多步筛选、转换、聚合),代码会变得冗长且可读性差。Stream 流的出现就是为了:

  1. 简化集合数据的处理逻辑
  2. 让代码更易读、更易维护
  3. 支持函数式编程风格
  4. 轻松实现并行处理(无需手动处理多线程)

二、什么是 Stream 流?

简单来说,Stream 流是 Java 8 引入的一套处理集合数据的 API,它像一个 "数据管道":

  • 数据源(通常是集合)的数据流入管道
  • 经过一系列中间操作(筛选、转换、排序等)
  • 最终通过终止操作得到结果

注意:Stream 流不会改变原始数据源,所有操作都是惰性执行的(直到终止操作才会真正执行)。

三、Stream 流的常用操作

在学习具体方法前,先明确一个重要概念:Stream 的操作分为中间操作和终止操作。

  • 中间操作:返回新的 Stream,支持链式调用(惰性执行,只有终止操作触发时才会执行)
  • 终止操作:返回具体结果(如集合、数值、布尔值等),触发整个流的计算

记住这个分类,能帮你理解 Stream 的执行逻辑。

        假设我们有一个学生列表,包含姓名、年龄、成绩等信息:

class Student {
    private String name;
    private int age;
    private double score;
    
    // 构造方法、getter、toString省略
}

List<Student> students = Arrays.asList(
    new Student("张三", 20, 85.5),
    new Student("李四", 17, 92.0),
    new Student("王五", 22, 78.5),
    new Student("赵六", 19, 90.0)
);

(一)、常用中间操作(返回 Stream)

1. 筛选与切片

方法 作用 示例
filter(Predicate<T> p) 保留满足条件的元素 stream.filter(s -> s.getAge() > 18)
limit(long n) 截取前 n 个元素 stream.limit(3)(取前 3 个)
skip(long n) 跳过前 n 个元素 stream.skip(2)(跳过前 2 个)
distinct() 去重(依赖 equals ()) stream.distinct()

实战场景:从学生列表中获取年龄 > 18 的前 2 名(排除重复数据)

List<Student> result = students.stream()
        .filter(s -> s.getAge() > 18)
        .distinct()
        .limit(2)
        .collect(Collectors.toList());

2. 映射(类型转换)

方法 作用 示例
map(Function<T, R> f) 将 T 类型转换为 R 类型 stream.map(Student::getName)(转姓名)
mapToInt(ToIntFunction<T> f) 转换为 IntStream stream.mapToInt(Student::getAge)
mapToLong(ToLongFunction<T> f) 转换为 LongStream stream.mapToLong(User::getId)
mapToDouble(ToDoubleFunction<T> f) 转换为 DoubleStream stream.mapToDouble(Student::getScore)
flatMap(Function<T, Stream<R>> f) 将流中每个元素转换为子流并合并 见下方示例

flatMap 示例:将列表中的字符串拆分为单个字符(如 ["abc", "def"] → ['a','b','c','d','e','f'])

List<String> words = Arrays.asList("abc", "def");
List<Character> chars = words.stream()
        .flatMap(s -> {
            List<Character> list = new ArrayList<>();
            for (char c : s.toCharArray()) {
                list.add(c);
            }
            return list.stream(); // 每个字符串转成字符流
        })
        .collect(Collectors.toList());

3. 排序

方法 作用 示例
sorted() 自然排序(需元素实现 Comparable) stream.sorted()(如 String、Integer 的默认排序)
sorted(Comparator<T> c) 自定义排序 stream.sorted(Comparator.comparingInt(Student::getAge))

示例:按成绩降序排序,成绩相同按年龄升序

students.stream()
        .sorted(Comparator.comparingDouble(Student::getScore).reversed() // 成绩降序
                .thenComparingInt(Student::getAge)) // 年龄升序
        .forEach(System.out::println);

4. 其他中间操作

方法 作用 示例
peek(Consumer<T> c) 对流中元素执行操作(不改变元素),常用于调试 stream.peek(s -> System.out.println("处理中:" + s))

(二)、常用终止操作(返回具体结果)

1. 遍历与收集

方法 作用 示例
forEach(Consumer<T> c) 遍历元素 stream.forEach(System.out::println)
collect(Collector c) 收集流为集合 / 对象 见下方详细说明

collect 常用收集器(需导入java.util.stream.Collectors):

  • toList():转为 List → collect(Collectors.toList())
  • toSet():转为 Set → collect(Collectors.toSet())
  • toMap():转为 Map(需指定 key 和 value) → collect(Collectors.toMap(Student::getName, Student::getScore))
  • joining():字符串拼接(适用于 String 流) → stream.map(Student::getName).collect(Collectors.joining(","))(用逗号分隔)

2. 聚合计算(适用于数值流或对象流)

方法 作用 示例
count() 统计元素数量 long count = stream.count()
max(Comparator<T> c) 求最大值 stream.max(Comparator.comparingInt(Student::getAge))
min(Comparator<T> c) 求最小值 stream.min(Comparator.comparingDouble(Student::getScore))
sum()(数值流) 求和 stream.mapToInt(Student::getAge).sum()
average()(数值流) 求平均值 stream.mapToDouble(Student::getScore).average()

示例:求 18 岁以上学生的最高成绩

Optional<Double> maxScore = students.stream()
        .filter(s -> s.getAge() > 18)
        .map(Student::getScore)
        .max(Double::compare);

// Optional处理(避免空指针)
double score = maxScore.orElse(0.0);

3. 匹配判断

方法 作用 示例
allMatch(Predicate<T> p) 所有元素是否满足条件 stream.allMatch(s -> s.getAge() > 18)(是否全是成年人)
anyMatch(Predicate<T> p) 是否有元素满足条件 stream.anyMatch(s -> s.getScore() == 100)(是否有满分)
noneMatch(Predicate<T> p) 所有元素是否都不满足条件 stream.noneMatch(s -> s.getName().isEmpty())(是否没有空姓名)

示例:检查是否所有学生都及格(假设 60 分为及格线)

boolean allPass = students.stream()
        .allMatch(s -> s.getScore() >= 60);

4. 查找元素

方法 作用 示例
findFirst() 返回第一个元素 stream.findFirst()
findAny() 返回任意一个元素(并行流中效率高) stream.findAny()

示例:获取第一个成年人学生

Optional<Student> firstAdult = students.stream()
        .filter(s -> s.getAge() > 18)
        .findFirst();

5. 规约(复杂聚合)

方法 作用 示例
reduce(T identity, BinaryOperator<T> accumulator) 从初始值开始,将元素累加计算 见下方示例

示例:计算所有学生的年龄总和(用 reduce 实现)

int totalAge = students.stream()
        .mapToInt(Student::getAge)
        .reduce(0, (a, b) -> a + b); // 0是初始值,a是累加结果,b是当前元素

(三)、并行流相关

当数据量很大时,我们可以轻松将串行流转换为并行流

并行流会自动利用多核处理器进行计算,无需我们手动创建线程池,这对于大数据处理非常有用。

方法 作用 示例
parallel() 将串行流转为并行流 stream.parallel()
sequential() 将并行流转为串行流 stream.sequential()

示例:大数据量下并行计算平均成绩

// 数据量越大,并行流优势越明显
double avg = students.parallelStream() // 直接获取并行流
        .mapToDouble(Student::getScore)
        .average()
        .orElse(0.0);

四、初学者常见问题

  1. Stream 只能用一次:一个 Stream 执行终止操作后就会关闭,再次使用会报错

    Stream<Student> stream = students.stream();
    stream.forEach(System.out::println); // 终止操作,流关闭
    stream.count(); // 报错:java.lang.IllegalStateException: stream has already been operated upon or closed
    
  2. 区分 map 和 flatMap:map 是 "一对一" 转换,flatMap 是 "一对多" 并合并流(如将列表中的列表展开)

  3. Optional 的使用:max ()、findFirst () 等方法返回 Optional,需用 orElse ()、ifPresent () 等方法处理,避免空指针

五、总结

Stream 流的方法虽然多,但核心是围绕 "数据管道" 的思想设计的。记住:先通过中间操作构建处理流程,再用终止操作获取结果

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐