强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Java 完全指南 / 15 - Stream API:流操作、并行流、收集器、Optional

15 - Stream API:流操作、并行流、收集器、Optional

Stream 基础

import java.util.*;
import java.util.stream.*;

public class StreamBasics {
    public static void main(String[] args) {
        List<String> names = List.of("张三", "李四", "王五", "赵六", "钱七", "孙八");

        // 1. 创建流
        Stream<String> s1 = names.stream();                    // 顺序流
        Stream<String> s2 = names.parallelStream();            // 并行流
        Stream<Integer> s3 = Stream.of(1, 2, 3, 4, 5);        // 直接创建
        Stream<Integer> s4 = Stream.iterate(0, n -> n + 2);    // 无限流
        Stream<Double> s5 = Stream.generate(Math::random);     // 生成流
        IntStream s6 = IntStream.range(1, 11);                 // 1..10
        IntStream s7 = IntStream.rangeClosed(1, 10);           // 1..10

        // 2. 中间操作(惰性,返回新 Stream)
        List<String> result = names.stream()
            .filter(name -> name.length() > 1)    // 过滤
            .map(String::toUpperCase)              // 映射
            .sorted()                              // 排序
            .limit(3)                              // 取前3个
            .distinct()                            // 去重
            .skip(1)                               // 跳过1个
            .toList();                             // 收集为 List

        System.out.println(result);

        // 3. 终端操作(触发执行)
        long count = names.stream().filter(n -> n.startsWith("张")).count();
        System.out.println("姓张的: " + count);

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

核心操作详解

filter / map / flatMap

public class StreamOperators {
    record Employee(String name, String dept, double salary) {}

    public static void main(String[] args) {
        List<Employee> employees = List.of(
            new Employee("张三", "技术部", 15000),
            new Employee("李四", "技术部", 20000),
            new Employee("王五", "市场部", 12000),
            new Employee("赵六", "市场部", 18000),
            new Employee("钱七", "技术部", 25000)
        );

        // filter —— 过滤
        List<Employee> techEmps = employees.stream()
            .filter(e -> e.dept().equals("技术部"))
            .toList();

        // map —— 一对一映射
        List<String> names = employees.stream()
            .map(Employee::name)
            .toList();

        // mapToDouble / mapToInt —— 基本类型流(避免装箱)
        double totalSalary = employees.stream()
            .mapToDouble(Employee::salary)
            .sum();

        double avgSalary = employees.stream()
            .mapToDouble(Employee::salary)
            .average()
            .orElse(0);

        // flatMap —— 一对多映射(扁平化)
        List<List<Integer>> nested = List.of(
            List.of(1, 2, 3),
            List.of(4, 5),
            List.of(6, 7, 8, 9)
        );
        List<Integer> flat = nested.stream()
            .flatMap(Collection::stream)
            .toList();
        System.out.println(flat);  // [1, 2, 3, 4, 5, 6, 7, 8, 9]

        // peek —— 调试用(不改变流)
        List<String> debugResult = names.stream()
            .peek(n -> System.out.println("处理: " + n))
            .filter(n -> n.length() > 1)
            .peek(n -> System.out.println("  通过过滤: " + n))
            .toList();
    }
}

收集器(Collectors)

import java.util.stream.Collectors;

public class CollectorDemo {
    record Product(String category, String name, double price) {}

    public static void main(String[] args) {
        List<Product> products = List.of(
            new Product("电子", "手机", 3999),
            new Product("电子", "耳机", 299),
            new Product("食品", "牛奶", 58),
            new Product("食品", "面包", 12),
            new Product("电子", "平板", 2999),
            new Product("食品", "果汁", 15)
        );

        // toList / toSet
        List<String> names = products.stream().map(Product::name).toList();
        Set<String> categories = products.stream().map(Product::category).collect(Collectors.toSet());

        // toMap
        Map<String, Double> priceMap = products.stream()
            .collect(Collectors.toMap(Product::name, Product::price));

        // 分组 groupingBy
        Map<String, List<Product>> grouped = products.stream()
            .collect(Collectors.groupingBy(Product::category));
        // {电子=[手机,耳机,平板], 食品=[牛奶,面包,果汁]}

        // 分组 + 聚合
        Map<String, Double> categoryTotal = products.stream()
            .collect(Collectors.groupingBy(Product::category,
                     Collectors.summingDouble(Product::price)));

        Map<String, Long> categoryCount = products.stream()
            .collect(Collectors.groupingBy(Product::category, Collectors.counting()));

        // 分区 partitioningBy(分为 true/false 两组)
        Map<Boolean, List<Product>> expensive = products.stream()
            .collect(Collectors.partitioningBy(p -> p.price() > 1000));

        // joining 字符串拼接
        String joined = products.stream()
            .map(Product::name)
            .collect(Collectors.joining(", ", "[", "]"));
        System.out.println(joined);  // [手机, 耳机, 牛奶, 面包, 平板, 果汁]

        // summarizing 统计
        DoubleSummaryStatistics stats = products.stream()
            .mapToDouble(Product::price)
            .summaryStatistics();
        System.out.printf("总数: %d, 总和: %.0f, 平均: %.0f, 最大: %.0f, 最小: %.0f%n",
            stats.getCount(), stats.getSum(), stats.getAverage(),
            stats.getMax(), stats.getMin());
    }
}

收集器速查表

收集器 说明 示例
toList() 收集为 List .toList()
toSet() 收集为 Set Collectors.toSet()
toMap(keyFn, valFn) 收集为 Map Collectors.toMap(...)
groupingBy(fn) 按条件分组 Collectors.groupingBy(...)
partitioningBy(pred) 按谓词分区 Collectors.partitioningBy(...)
joining(delim) 字符串拼接 Collectors.joining(",")
counting() 计数 Collectors.counting()
summingDouble(fn) 求和 Collectors.summingDouble(...)
averagingDouble(fn) 求平均 Collectors.averagingDouble(...)
maxBy(cmp) 最大值 Collectors.maxBy(...)
minBy(cmp) 最小值 Collectors.minBy(...)
reducing(init, fn) 归约 Collectors.reducing(0, ...)

Optional

import java.util.Optional;

public class OptionalDemo {
    public static void main(String[] args) {
        // 创建 Optional
        Optional<String> empty = Optional.empty();
        Optional<String> present = Optional.of("hello");
        Optional<String> nullable = Optional.ofNullable(null);

        // 判断
        System.out.println(present.isPresent());   // true
        System.out.println(empty.isPresent());      // false
        System.out.println(empty.isEmpty());        // true (JDK 11+)

        // 获取值
        String val1 = present.get();                // 直接获取(可能抛异常)
        String val2 = present.orElse("default");    // 为空时返回默认值
        String val3 = empty.orElse("default");      // "default"
        String val4 = empty.orElseGet(() -> computeDefault());  // 延迟计算
        // String val5 = empty.orElseThrow();         // 抛 NoSuchElementException

        // 链式操作
        String upper = Optional.of("hello")
            .map(String::toUpperCase)         // Optional<String>
            .filter(s -> s.length() > 3)      // Optional<String>
            .orElse("HI");
        System.out.println(upper);  // HELLO

        // flatMap —— 当映射函数返回 Optional 时使用
        Optional<String> result = Optional.of("[email protected]")
            .flatMap(OptionalDemo::extractDomain);
        result.ifPresent(System.out::println);  // email.com

        // 实际用法:替代 null 检查
        String city = Optional.ofNullable(getUser())
            .map(User::getAddress)
            .map(Address::getCity)
            .orElse("未知城市");
    }

    record User(Address address) {}
    record Address(String city) {}
    static User getUser() { return null; }

    static Optional<String> extractDomain(String email) {
        int idx = email.indexOf('@');
        return idx > 0 ? Optional.of(email.substring(idx + 1)) : Optional.empty();
    }

    static String computeDefault() {
        System.out.println("计算默认值...");
        return "computed";
    }
}

Optional 使用规范

场景 正确做法 错误做法
返回值可能为空 Optional<User> find() User find() 返回 null
判断存在后消费 opt.ifPresent(...) if (opt.isPresent()) opt.get()
提供默认值 opt.orElse(...) opt.isPresent() ? opt.get() : ...
字段类型 不要用 Optional 做字段 Optional<String> name

并行流

public class ParallelStreamDemo {
    public static void main(String[] args) {
        List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000).boxed().toList();

        // 顺序流
        long t1 = System.nanoTime();
        long sum1 = numbers.stream().mapToLong(Long::valueOf).sum();
        long t2 = System.nanoTime();

        // 并行流
        long sum2 = numbers.parallelStream().mapToLong(Long::valueOf).sum();
        long t3 = System.nanoTime();

        System.out.println("顺序: " + sum1 + " 耗时: " + (t2 - t1) / 1_000_000 + "ms");
        System.out.println("并行: " + sum2 + " 耗时: " + (t3 - t2) / 1_000_000 + "ms");

        // 自定义并行度
        var pool = java.util.concurrent.ForkJoinPool.commonPool();
        System.out.println("并行度: " + pool.getParallelism());
    }
}
场景 适合并行流 不适合并行流
大数据量 + 简单操作
小数据量 ❌ 线程开销更大
有状态操作(sorted) ❌ 需要同步
涉及共享可变状态 ❌ 数据竞争
I/O 密集型 ❌ 阻塞线程池

⚠️ 注意事项

  1. Stream 只能消费一次 — 重复使用会抛 IllegalStateException
  2. 惰性求值 — 中间操作不执行,直到遇到终端操作。
  3. 避免副作用 — 不要在 Stream 操作中修改外部变量。
  4. Optional 不要 get() 前不检查 — 用 orElse/ifPresent

💡 技巧

  1. Stream 转基本类型数组

    int[] arr = list.stream().mapToInt(Integer::intValue).toArray();
    
  2. 多条件排序

    list.sort(Comparator.comparing(Student::getScore).reversed()
                        .thenComparing(Student::getName));
    
  3. 去重(按某字段)

    list.stream().collect(Collectors.toMap(User::getId, u -> u, (a, b) -> a))
        .values();
    

🏢 业务场景

  • 数据转换: 从数据库查询结果转换为 DTO 列表。
  • 统计报表: 按部门分组统计薪资总额、平均值。
  • 数据清洗: 过滤无效数据、去重、格式转换。
  • 配置处理: 从 properties 文件读取并分组配置项。

📖 扩展阅读