Java 8 Stream 流操作全解析

发布于:2025-05-23 ⋅ 阅读:(17) ⋅ 点赞:(0)

一、Stream 流简介

Java 8 引入的 Stream 提供了一种高效、声明式处理集合数据的方式,支持顺序和并行操作,核心特点包括:

  • 链式调用:通过组合中间操作(如 filter, map)和终端操作(如 collect, forEach)实现复杂逻辑。
  • 延迟执行:只有终端操作触发时才会执行中间操作。
  • 不可重用:每个流只能被消费一次。

二、Stream 流核心操作

1. 创建 Stream

  • 集合创建Collection.stream()parallelStream()

    List<String> list = Arrays.asList("a", "b", "c");
    Stream<String> stream = list.stream();
    
  • 数组创建Arrays.stream(array)

    String[] arr = {"a", "b", "c"};
    Stream<String> stream = Arrays.stream(arr);
    
  • 静态方法Stream.of() 或生成无限流 Stream.iterate(), Stream.generate()

    Stream<Integer> numbers = Stream.of(1, 2, 3);
    Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 2); // 0, 2, 4, ...
    

2. 中间操作(Intermediate Operations)

filter(Predicate):过滤数据

作用:根据条件筛选元素,保留满足条件的元素。
示例

List<String> filtered = list.stream()
    .filter(s -> s.startsWith("a"))  // 保留以"a"开头的字符串
    .collect(Collectors.toList());

最佳实践

1. 简单条件过滤

筛选出符合条件的元素。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

// 筛选所有偶数
List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList()); // [2, 4, 6]
2. 多条件组合

使用逻辑运算符 &&(与)、||(或)组合条件。

List<String> words = Arrays.asList("apple", "banana", "cherry", "date");

// 筛选长度大于5 且 以字母a开头的单词
List<String> result = words.stream()
    .filter(s -> s.length() > 5 && s.startsWith("b"))
    .collect(Collectors.toList()); // ["banana"]
3. 过滤对象集合

根据对象属性筛选数据。

class User {
    String name;
    int age;
    // 构造方法、getter/setter 省略
}

List<User> users = Arrays.asList(
    new User("Alice", 25),
    new User("Bob", 17),
    new User("Charlie", 30)
);

// 筛选年龄 >= 18 的用户
List<User> adults = users.stream()
    .filter(user -> user.getAge() >= 18)
    .collect(Collectors.toList()); // [Alice, Charlie]
4. 过滤 null

使用 Objects::nonNull 过滤掉 null 元素。

List<String> listWithNulls = Arrays.asList("a", null, "b", null, "c");

// 过滤掉所有 null 值
List<String> nonNullList = listWithNulls.stream()
    .filter(Objects::nonNull)
    .collect(Collectors.toList()); // ["a", "b", "c"]

2. map(Function<T, R>):转换元素

map 是 Java Stream 中最核心的中间操作之一,用于将流中的元素一对一转换为另一种形式。它的本质是通过一个函数(Function<T, R>)对每个元素进行映射,生成新的元素流。以下是 map 的深入解析,涵盖使用场景、底层机制、最佳实践与常见问题。


一、map 的核心特性
  1. 一对一转换:每个输入元素对应一个输出元素,元素数量不变。
  2. 类型转换:输入类型 T 可转换为任意输出类型 R(如 StringInteger)。
  3. 惰性求值:只有终端操作触发时才会执行映射逻辑。
  4. 无副作用:理想情况下,映射函数不修改外部状态(符合函数式编程原则)。

作用:将元素转换为另一种类型或提取特定属性。

1. 简单类型转换

// 将字符串转换为大写
List<String> upperCaseList = Arrays.asList("apple", "banana", "cherry").stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList()); // ["APPLE", "BANANA", "CHERRY"]
2. 提取对象属性
// 从User对象中提取name属性
List<String> names = users.stream()
    .map(User::getName)
    .collect(Collectors.toList());
3. 复杂转换逻辑
// 将字符串转换为自定义DTO对象
List<DataDTO> dtos = strings.stream()
    .map(s -> {
        DataDTO dto = new DataDTO();
        dto.setValue(s.length());
        dto.setLabel(s.toUpperCase());
        return dto;
    })
    .collect(Collectors.toList());

3. flatMap(Function<T, Stream>):扁平化嵌套结构

作用:将嵌套集合(如 List<List<T>>)展开为单一流。
示例

List<List<String>> nestedList = Arrays.asList(
    Arrays.asList("a", "b"),
    Arrays.asList("c", "d")
);
List<String> flatList = nestedList.stream()
    .flatMap(Collection::stream)  // 将每个List<String>转换为Stream<String>
    .collect(Collectors.toList()); // ["a", "b", "c", "d"]

典型场景

  • 处理数据库查询的多表关联结果。
  • 合并多个API响应的数据列表。

4. distinct():去重

作用:基于 equals()hashCode() 去重。
示例

List<Integer> unique = numbers.stream()
    .distinct()
    .collect(Collectors.toList()); // [1, 2, 3]

关键点

  • 自定义对象去重:需重写 equals()hashCode()

    class User {
        private Long id;
        @Override
        public boolean equals(Object o) { /* 基于id比较 */ }
        @Override
        public int hashCode() { /* 基于id生成 */ }
    }
    
  • 性能注意:对大数据集去重可能消耗内存,可结合 limit 分批次处理。


5. sorted():排序

作用:按自然顺序或自定义比较器排序。
示例

List<String> sorted = list.stream()
    .sorted(Comparator.reverseOrder())  // 逆序排序
    .collect(Collectors.toList());

优化建议

  • 尽早过滤:先 filter 减少待排序数据量。
  • 避免频繁排序:对需要多次排序的场景,考虑转换为有序集合(如 TreeSet)。

6. limit(n) 和 skip(n):分页控制

作用skip 跳过前N个元素,limit 限制返回数量。
示例

List<Integer> result = Stream.iterate(0, n -> n + 1)
    .skip(5)   // 跳过0-4,从5开始
    .limit(10) // 取5-14
    .collect(Collectors.toList());

应用场景

  • 分页查询:模拟数据库分页。

    int page = 2, size = 10;
    List<User> users = allUsers.stream()
        .skip((page - 1) * size)
        .limit(size)
        .collect(Collectors.toList());
    
  • 性能注意:对非顺序流(如并行流),skiplimit 可能无法保证预期结果。

  • 3. 终端操作(Terminal Operations)

  • 遍历元素forEach(Consumer<T>)

    list.stream().forEach(System.out::println);
    
  • 收集结果collect(Collector)

    List<String> list = stream.collect(Collectors.toList());
    Set<String> set = stream.collect(Collectors.toSet());
    String joined = stream.collect(Collectors.joining(", "));
    
  • 统计数量count()

    long count = list.stream().filter(s -> s.length() > 3).count();
    
  • 匹配检查

    • anyMatch(Predicate<T>):至少一个元素匹配。
    • allMatch(Predicate<T>):所有元素匹配。
    • noneMatch(Predicate<T>):没有元素匹配。
    boolean hasA = list.stream().anyMatch(s -> s.contains("a"));
    
  • 查找元素

    • findFirst():返回第一个元素(Optional<T>)。
    • findAny():适用于并行流,返回任意元素。
    Optional<String> first = list.stream().findFirst();
    
  • 归约操作reduce(BinaryOperator<T>)

    Optional<Integer> sum = Stream.of(1, 2, 3).reduce(Integer::sum); // 6
    

三、高级用法

1. 分组与分区
  • 分组Collectors.groupingBy()

    Map<Integer, List<String>> groupByLength = list.stream()
        .collect(Collectors.groupingBy(String::length)); // 按字符串长度分组
    
  • 分区Collectors.partitioningBy()

    Map<Boolean, List<String>> partition = list.stream()
        .collect(Collectors.partitioningBy(s -> s.length() > 3)); // 按条件分为两组
    
2. 并行流处理
  • 创建并行流.parallel()parallelStream()

    List<String> result = list.parallelStream()
        .filter(s -> s.length() > 3)
        .collect(Collectors.toList());
    
  • 注意事项

    • 确保操作线程安全(如避免修改共享变量)。
    • 并行流可能不适用于小数据量或复杂中间操作。
3. 原始类型流
  • 避免装箱开销:使用 IntStream, LongStream, DoubleStream

    IntStream.range(1, 5).forEach(System.out::println); // 1, 2, 3, 4
    LongStream.of(10L, 20L).sum();
    

四、实际应用场景示例
1. 数据转换与过滤
// 从用户列表中提取成年用户的姓名
List<String> adultNames = users.stream()
    .filter(user -> user.getAge() >= 18)
    .map(User::getName)
    .collect(Collectors.toList());
2. 统计与汇总
// 计算订单总金额
double totalAmount = orders.stream()
    .mapToDouble(Order::getAmount)
    .sum();
3. 复杂集合处理
// 将多个订单的商品列表合并并去重
Set<String> allProducts = orders.stream()
    .flatMap(order -> order.getProducts().stream())
    .collect(Collectors.toSet());
4. 分组统计
// 按部门分组统计员工平均工资
Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)
    );

五、注意事项与最佳实践
  1. 避免副作用:在流操作中不要修改外部变量,尤其是在并行流中。
  2. 优先使用无状态操作:如 filter, mapsorted, distinct 更高效。
  3. 谨慎使用并行流:仅在数据量大且操作耗时的情况下考虑并行化。
  4. 减少装箱开销:对数值操作使用原始类型流(IntStream 等)。
  5. 合理使用短路操作:如 findFirst(), limit() 可提前终止流处理。

六、与传统循环的对比
场景 传统循环 Stream 流
简单遍历 直接易读 代码更简洁,但可能略微性能开销
复杂数据处理 需多层嵌套循环,代码冗长 链式调用,逻辑清晰
并行处理 需手动管理线程和同步 通过 .parallel() 自动并行化
函数式编程支持 需额外工具类配合 原生支持 Lambda 和方法引用

通过掌握 Stream 流的常见用法,可以显著提升代码的可读性和开发效率,尤其在处理集合数据时,能够以更简洁的方式实现复杂的数据操作。


网站公告

今日签到

点亮在社区的每一天
去签到