Stream API 与 Lambda:高效处理集合
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 表达式主要用在函数式接口(只有一个抽象方法的接口)出现的地方,例如 Comparator、Runnable 或者 Stream 的各种操作中。
Stream:流水线式的数据处理
Stream 并不是数据结构,它不存储数据,而是像一个高级迭代器,支持对数据源(集合、数组、I/O 等)进行函数式操作。Stream 的特点是:
- 声明式:表达“做什么”而非“怎么做”。
- 流水线:多个操作可以串联起来,形成一个流水线。
- 内部迭代:迭代过程由 Stream 内部完成,无需编写显式循环。
- 惰性求值:中间操作不会立即执行,只有遇到终端操作时才会触发计算。
一个 Stream 操作通常包含三个阶段:
- 创建 Stream:从集合、数组等数据源获取流。
- 中间操作:对数据进行过滤、映射、排序等,返回一个新的 Stream。这类操作是惰性的。
- 终端操作:产生最终结果,例如收集到列表、计算总和、打印每个元素,或者只是判断是否存在某个元素。终端操作执行后,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 提供了专门处理基本类型的流:IntStream、LongStream、DoubleStream。它们有额外的高效操作,如 sum、average、range 等。
// 创建一个 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 只是调用现有方法时,方法引用更清晰。
- 区分
map和flatMap:一个用于一对一的转换,另一个用于一对多的平铺。
总结
Stream API 和 Lambda 表达式共同构成了 Java 函数式编程的基石。它们让你能够以流水线的方式表达数据处理逻辑,代码精简且可读性高。掌握 filter、map、reduce、collect 等核心操作,你就能告别大量冗长的循环和临时变量,写出更现代、更高效的 Java 代码。实际开发中,多练习将复杂的业务逻辑拆解为一系列 Stream 操作,你会越来越感受到它的优雅与强大。