Stream API 与 Lambda:高效处理集合

FreeGuideOnline 最新 2026-06-17

Stream API 与 Lambda:高效处理集合

在现代 Java 开发中,处理集合数据是一项极其常见的任务。传统的方式往往需要编写大量循环和条件判断,代码既冗长又容易出错。Java 8 引入的 Lambda 表达式与 Stream API 彻底改变了这一局面,让集合操作变得简洁、声明式且高效。本教程将带你从零开始,掌握这一强大组合的核心用法。

为什么需要 Stream 和 Lambda?

在没有 Lambda 和 Stream 的年代,哪怕是对一个列表进行过滤和转换,也需要写这样的代码:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
List<String> result = new ArrayList<>();
for (String name : names) {
    if (name.startsWith("A")) {
        result.add(name.toUpperCase());
    }
}

这段代码不仅篇幅较长,更重要的是它描述的是“如何做”(命令式),而不是“做什么”(声明式)。Stream 与 Lambda 的目标就是让你直接表达意图,把精力放在业务逻辑上。

Lambda 表达式快速入门

Lambda 本质上是一种简洁表示可传递匿名函数的方式。它的语法可以理解为:

(参数列表) -> { 函数体 }

如果函数体只有一行,可以省略大括号和 return 关键字;如果参数类型可以推断,也可以省略类型。例如:

// 无参数,直接返回一个值
() -> "Hello"

// 单参数,省略类型和小括号
s -> s.length()

// 多参数,带有类型声明
(int a, int b) -> a + b

// 多行代码块
(x, y) -> {
    int max = x > y ? x : y;
    return max;
}

Lambda 表达式主要用在函数式接口(只有一个抽象方法的接口)出现的地方,例如 ComparatorRunnable 或者 Stream 的各种操作中。

Stream:流水线式的数据处理

Stream 并不是数据结构,它不存储数据,而是像一个高级迭代器,支持对数据源(集合、数组、I/O 等)进行函数式操作。Stream 的特点是:

  • 声明式:表达“做什么”而非“怎么做”。
  • 流水线:多个操作可以串联起来,形成一个流水线。
  • 内部迭代:迭代过程由 Stream 内部完成,无需编写显式循环。
  • 惰性求值:中间操作不会立即执行,只有遇到终端操作时才会触发计算。

一个 Stream 操作通常包含三个阶段:

  1. 创建 Stream:从集合、数组等数据源获取流。
  2. 中间操作:对数据进行过滤、映射、排序等,返回一个新的 Stream。这类操作是惰性的。
  3. 终端操作:产生最终结果,例如收集到列表、计算总和、打印每个元素,或者只是判断是否存在某个元素。终端操作执行后,Stream 就会关闭。

创建 Stream 的常见方式

// 从集合创建
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> streamFromList = list.stream();

// 从数组创建
String[] array = {"x", "y", "z"};
Stream<String> streamFromArray = Arrays.stream(array);

// 使用 Stream.of
Stream<Integer> streamOf = Stream.of(1, 2, 3);

// 创建无限流(需要配合 limit 使用)
Stream<Double> randomStream = Stream.generate(Math::random).limit(10);
Stream<Integer> iterateStream = Stream.iterate(0, n -> n + 2).limit(5); // 0,2,4,6,8

核心中间操作

中间操作会返回一个新的 Stream,可以连续调用。下面是最常用的几种:

filter:过滤元素

接收一个 Predicate(返回 boolean 的 Lambda),保留符合条件的元素。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
// 只保留偶数
List<Integer> evens = numbers.stream()
        .filter(n -> n % 2 == 0)
        .collect(Collectors.toList()); // [2, 4, 6]

map:转换元素

接收一个 Function,将每个元素映射成另一种形式。

List<String> words = Arrays.asList("hello", "stream", "lambda");
// 获取每个单词的长度
List<Integer> lengths = words.stream()
        .map(String::length)  // 方法引用,等价于 s -> s.length()
        .collect(Collectors.toList()); // [5, 6, 6]

flatMap:扁平化映射

当每个元素本身又是一个集合或数组,而你希望将所有子元素平铺到一个流中时使用。

List<List<String>> listOfLists = Arrays.asList(
        Arrays.asList("a", "b"),
        Arrays.asList("c", "d")
);
// 将两层列表压平为单层流
List<String> flatList = listOfLists.stream()
        .flatMap(List::stream)
        .collect(Collectors.toList()); // [a, b, c, d]

sorted:排序

可以使用默认排序,也可以传入自定义比较器。

List<String> names = Arrays.asList("Bob", "Alice", "Charlie");
List<String> sortedNames = names.stream()
        .sorted()
        .collect(Collectors.toList()); // [Alice, Bob, Charlie]

// 按长度降序
List<String> sortedByLength = names.stream()
        .sorted((a, b) -> Integer.compare(b.length(), a.length()))
        .collect(Collectors.toList());

distinct:去重

根据 equals 方法去除重复元素。

List<Integer> nums = Arrays.asList(1, 2, 2, 3, 3, 3);
List<Integer> distinctNums = nums.stream()
        .distinct()
        .collect(Collectors.toList()); // [1, 2, 3]

limit 与 skip:截取和跳过

limit(n) 保留流的前 n 个元素,skip(n) 跳过前 n 个元素。两者常用于分页或取前几条数据。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.stream().limit(3).forEach(System.out::println); // 1 2 3
numbers.stream().skip(2).forEach(System.out::println); // 3 4 5

终端操作:触发流水线执行

终端操作执行后,流会被消耗,无法再次使用。常见的终端操作有:

collect:收集结果

将流中的元素收集到集合、字符串等容器中。通常借助 Collectors 工具类。

List<String> collected = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
// 收集到指定集合类型
ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));
// 连接成字符串
String joined = stream.collect(Collectors.joining(", "));

forEach:遍历每个元素

对流中的每个元素执行某个操作。注意,forEach 并不保证顺序(特别是在并行流中),如果需要按顺序处理可以使用 forEachOrdered

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

reduce:归约操作

将流中的元素反复结合起来,得到一个值。适用于求和、求积、查找最大值等。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
// 求和:初始值为0,累加器为 (a,b) -> a + b
int sum = numbers.stream().reduce(0, (a, b) -> a + b); // 10
// 也可以用方法引用
int sum2 = numbers.stream().reduce(0, Integer::sum);

// 没有初始值的 reduce 返回 Optional
Optional<Integer> max = numbers.stream().reduce(Integer::max);

count、anyMatch、allMatch、noneMatch

用于计数和条件匹配。

long count = numbers.stream().count();
boolean anyEven = numbers.stream().anyMatch(n -> n % 2 == 0); // true
boolean allPositive = numbers.stream().allMatch(n -> n > 0); // true
boolean noneNegative = numbers.stream().noneMatch(n -> n < 0); // true

findFirst 与 findAny

返回流中的某个元素,返回类型为 Optional。在并行流中 findAny 性能更好,但结果不确定。

Optional<String> first = names.stream().findFirst();
Optional<String> any = names.parallelStream().findAny();

数值流与基本类型优化

由于基本数据类型与对象的装箱拆箱会带来性能损耗,Stream API 提供了专门处理基本类型的流:IntStreamLongStreamDoubleStream。它们有额外的高效操作,如 sumaveragerange 等。

// 创建一个 1 到 10 的整数流
IntStream.rangeClosed(1, 10).sum(); // 55

// 将对象流映射为数值流
int totalLength = words.stream()
        .mapToInt(String::length)
        .sum();

// 将数值流转换回对象流
Stream<Integer> boxed = IntStream.of(1, 2, 3).boxed();

方法引用:让代码更简洁

当 Lambda 表达式仅仅调用一个已经存在的方法时,可以使用方法引用(::)进一步简化。它主要有四种形式:

  • 静态方法引用ClassName::staticMethod,如 Integer::parseInt
  • 实例方法引用(特定对象)instance::method,如 System.out::println
  • 实例方法引用(任意对象)ClassName::instanceMethod,如 String::length,相当于 (s) -> s.length()
  • 构造方法引用ClassName::new,如 ArrayList::new

在之前的例子中已经多次出现方法引用,它让函数式代码更易读。

实战案例:综合运用 Stream + Lambda

假设我们有 Person 类:

class Person {
    private String name;
    private int age;
    // 构造器、getter 省略
}

现在有一个 List<Person>,需要完成:筛选出年龄大于 18 的人,提取他们的名字,将名字转换为大写,按字母排序,最后收集到一个列表中。传统写法繁琐,而 Stream 写法如下:

List<Person> people = // ... 初始化

List<String> result = people.stream()
        .filter(p -> p.getAge() > 18)          // 过滤
        .map(Person::getName)                  // 获取名字
        .map(String::toUpperCase)              // 转大写
        .sorted()                              // 排序
        .collect(Collectors.toList());         // 收集

再比如,按年龄分组用户:

Map<Integer, List<Person>> byAge = people.stream()
        .collect(Collectors.groupingBy(Person::getAge));

统计每个年龄的人数:

Map<Integer, Long> countByAge = people.stream()
        .collect(Collectors.groupingBy(Person::getAge, Collectors.counting()));

并行流简介

如果你的数据量很大,并且操作是无状态、可独立并行的,可以通过 .parallelStream().parallel() 轻松将流转换为并行流。内部会使用 Fork/Join 池来加速处理。

long sum = numbers.parallelStream()
        .reduce(0, Integer::sum);

使用并行流时,要注意线程安全和操作本身的开销,并不是所有情况都适合并行。findAny 在并行流中能提升效率,但 forEach 的顺序可能无法保证,需要时使用 forEachOrdered

常见陷阱与最佳实践

  • Stream 不可复用:一个流一旦执行了终端操作,就不可再次使用。如果有再次处理的需求,需要重新从数据源创建流。
  • 避免在 Lambda 中修改外部变量:Lambda 引用的外部变量必须是事实不可变(effectively final)的,否则编译报错。如果你试图在流内部修改集合,会导致代码不可预测,尽量避免。
  • 注意惰性求值:中间操作在没有终端操作时不会执行,这有时会导致你认为“没有执行”的误判。
  • 谨慎使用并行流:对于小数据量或操作本身很轻量的场景,并行流可能因为线程调度开销反而更慢。
  • 优先使用方法引用:在 Lambda 只是调用现有方法时,方法引用更清晰。
  • 区分 mapflatMap:一个用于一对一的转换,另一个用于一对多的平铺。

总结

Stream API 和 Lambda 表达式共同构成了 Java 函数式编程的基石。它们让你能够以流水线的方式表达数据处理逻辑,代码精简且可读性高。掌握 filtermapreducecollect 等核心操作,你就能告别大量冗长的循环和临时变量,写出更现代、更高效的 Java 代码。实际开发中,多练习将复杂的业务逻辑拆解为一系列 Stream 操作,你会越来越感受到它的优雅与强大。