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

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()收集为 SetCollectors.toSet()
toMap(keyFn, valFn)收集为 MapCollectors.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 文件读取并分组配置项。

📖 扩展阅读