Java Stream API 实战指南:3个核心概念让你告别循环嵌套

12 阅读4分钟

Java Stream API 实战指南:3个核心概念让你告别循环嵌套

前言:你还在写这种代码吗?

java

List<String> result = new ArrayList<>();
for (String s : list) {
    if (s.length() > 3) {
        result.add(s.toUpperCase());
    }
}

如果是,那这篇文章就是为你准备的。Java 8 的 Stream API 让你可以用一行代码搞定上述逻辑,而且更优雅、更高效、更易维护。

读完本文,你将掌握:

  • 4种 Stream 创建方式,应对不同数据源
  • 中间操作与终结操作的本质区别(避免掉坑)
  • 3个最常用 Collectors,从列表到映射全搞定

一、Stream 创建:流水线的起点

1. 从集合创建(最常用)

java

List<String> list = Arrays.asList("Java", "Stream", "Collection");

// 顺序流:保持元素原有顺序
Stream<String> stream = list.stream();

// 并行流:大数据量时自动多线程处理
Stream<String> parallelStream = list.parallelStream();

什么时候用并行流? 数据量超过 1 万元素,且操作无状态(如 filter、map)时考虑使用。

2. 直接创建元素流

java

// 快速创建流
Stream<String> fruits = Stream.of("Apple", "Banana", "Cherry");

// 创建空流(避免 NPE 的最佳实践)
Stream<String> empty = Stream.empty();

场景技巧:处理可能为 null 的集合时,用 Optional.ofNullable(list).orElseGet(Collections::emptyList).stream() 替代空判断。

3. 从数组创建

java

// 引用类型数组
String[] colors = {"Red", "Green", "Blue"};
Stream<String> colorStream = Arrays.stream(colors);

// 基本类型数组(避免自动装箱)
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);

// 截取数组部分(左闭右开)
IntStream part = Arrays.stream(numbers, 1, 4);  // [2, 3, 4]

性能提示:处理 int[]long[]double[] 时优先用 IntStream/LongStream/DoubleStream,避免大量装箱拆箱。

4. 从文件按行读取

java

try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {
    lines.filter(line -> !line.isBlank())
         .forEach(System.out::println);
} catch (IOException e) {
    e.printStackTrace();
}

适用场景:大文件处理(GB 级别),流式读取不会把整个文件加载到内存。

二、中间操作 vs 终结操作:理解惰性求值

核心对比

特性中间操作终结操作
返回值新的 Stream(支持链式调用)具体结果(List、Integer、void 等)
执行时机惰性执行(仅记录规则)触发实际处理
流状态可继续操作流被消费,不可再用

常用方法速查

中间操作: filter()map()sorted()distinct()limit()skip()

终结操作: forEach()collect()count()reduce()anyMatch()findFirst()

惰性求值的威力

看这个例子:

java

Stream.of(1, 2, 3, 4, 5, 6)
    .filter(n -> {
        System.out.println("filter: " + n);
        return n % 2 == 0;
    })
    .map(n -> {
        System.out.println("map: " + n);
        return n * n;
    })
    .collect(Collectors.toList());

输出:

plaintext

filter: 1
filter: 2
map: 2
filter: 3
filter: 4
map: 4
filter: 5
filter: 6
map: 6

关键洞察:

  • 只有遇到 collect() 时,整个流水线才开始执行
  • 每个元素流经所有中间操作,一次性处理完成
  • 如果加上 .limit(2),会在找到 2 个结果后立即停止,不会遍历全部数据

性能技巧:将 filter() 放在 map() 之前,尽早减少数据量。

三、Collectors 实战:3个常用收集器

1. toList():收集到列表

java

// 基础用法
List<String> fruits = Stream.of("Apple", "Banana", "Cherry")
                           .collect(Collectors.toList());

// 结合筛选
List<Integer> evens = Stream.of(1, 2, 3, 4, 5)
                           .filter(n -> n % 2 == 0)
                           .collect(Collectors.toList());

JDK 16+ 可直接用 .toList(),返回的是不可变列表。

2. toMap():收集到映射

java

// 基础:对象流 → Map
Map<String, String> userMap = users.stream()
    .collect(Collectors.toMap(
        User::getId,      // key
        User::getName     // value
    ));

// 处理重复键(保留旧值)
Map<String, String> safeMap = users.stream()
    .collect(Collectors.toMap(
        User::getId,
        User::getName,
        (oldVal, newVal) -> oldVal  // 合并策略
    ));

// 简单类型:数字 → 平方
Map<Integer, Integer> squares = Stream.of(1, 2, 3)
    .collect(Collectors.toMap(
        n -> n,
        n -> n * n
    ));
// 结果:{1=1, 2=4, 3=9}

注意:不指定合并策略时,遇到重复键会抛出 IllegalStateException

3. joining():字符串拼接

java

// 直接拼接(无分隔符)
String s1 = Stream.of("Java", "Stream").collect(Collectors.joining());
// "JavaStream"

// 指定分隔符
String s2 = Stream.of("Java", "Stream").collect(Collectors.joining("-"));
// "Java-Stream"

// 完整格式:分隔符 + 前缀 + 后缀
String s3 = Stream.of("Java", "Stream", "API")
    .collect(Collectors.joining(", ", "[", "]"));
// "[Java, Stream, API]"

// 非字符串流:先转换
String nums = Stream.of(1, 2, 3)
    .map(String::valueOf)
    .collect(Collectors.joining("|"));
// "1|2|3"

性能提示joining() 底层使用 StringBuilder,比手动 + 拼接快很多,适合拼接大量字符串。

四、实战速查表

需求代码片段
过滤奇数.filter(n -> n % 2 == 0)
转大写.map(String::toUpperCase)
去重.distinct()
排序.sorted() / .sorted(Comparator.reverseOrder())
取前3个.limit(3)
跳过前2个.skip(2)
转数组.toArray(String[]::new)
计数.count()
求和.mapToInt(Integer::intValue).sum()
任意匹配.anyMatch(n -> n > 10)

结语:这才是现代 Java 的样子

Stream API 不是炫技工具,而是让代码更接近业务意图的方式。当你习惯用声明式风格思考数据流,会发现代码变得:

  • 更简洁:一行替代多行循环嵌套
  • 更安全:减少索引错误、空指针风险
  • 更高效:惰性求值 + 并行流优化性能

评论区交流: 你在项目中用 Stream API 遇到过哪些坑?或者有什么独门技巧?欢迎分享!