5Effective Java(第3版)

Lambda和Stream

函数式编程实践

Lambda表达式Stream API方法引用并行流

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第5章:Lambda和Stream(Lambdas and Streams)

一、导读

1.1 本章学习目标

第5章探讨 Java 8 引入的最重要的语言特性:Lambda 表达式和 Stream API。这两个特性从根本上改变了 Java 的编程范式,使得函数式编程风格成为可能。本章通过10条经验法则,帮助你掌握 Lambda 和 Stream 的正确使用方式。

本章的核心学习目标包括:

  • 理解 Lambda 表达式的本质:掌握 Lambda 的语法、类型推断和捕获机制。
  • 掌握方法引用的使用:理解四种方法引用的形式,知道何时用方法引用代替 Lambda。
  • 学会正确使用 Stream API:掌握 Stream 的创建、转换和终端操作。
  • 理解并行流的陷阱:知道何时使用并行流,何时避免。
  • 掌握 Collector 的定制:学会编写自定义收集器。

1.2 为什么第五章如此重要

Lambda 和 Stream 的引入是 Java 历史上最大的一次语言变革。它们不仅改变了代码的写法,更改变了程序员思考问题的方式。函数式编程的核心思想——不可变数据、纯函数、高阶函数——通过 Lambda 和 Stream 融入了 Java 的主流编程实践。

正确使用 Lambda 和 Stream,代码会变得更简洁、更具表达力、更易于并行化。错误使用则会导致代码难以调试、性能下降。

1.3 核心问题引导

Lambda 表达式的类型是什么?它与函数式接口有什么关系?

方法引用有哪四种形式?什么时候应该用方法引用代替 Lambda?

Stream 的中间操作和终端操作有什么区别?

并行流在什么情况下能提升性能?什么情况下反而更慢?

如何编写自定义的 Collector?


二、核心概念详解

5.1 Lambda 表达式(Item 42-43)

5.1.1 Lambda 的基本语法

java
// 完整语法
(String s1, String s2) -> { return s1.compareTo(s2); }

// 简化:省略类型
(s1, s2) -> { return s1.compareTo(s2); }

// 简化:单表达式省略大括号和 return
(s1, s2) -> s1.compareTo(s2)

// 简化:单参数省略括号
s -> s.length()

// 无参数
() -> System.out.println("Hello")

5.1.2 函数式接口

Lambda 表达式的类型是函数式接口(只有一个抽象方法的接口):

java
// 常用函数式接口
Runnable r = () -> System.out.println("Hello");
Callable<String> c = () -> "Hello";
Comparator<String> cmp = (s1, s2) -> s1.compareTo(s2);
Consumer<String> cons = s -> System.out.println(s);
Function<String, Integer> fn = s -> s.length();
Predicate<String> pred = s -> s.isEmpty();
Supplier<String> sup = () -> "Hello";

5.1.3 方法引用

方法引用是 Lambda 的简写形式,有四种类型:

java
// 1. 静态方法引用
Function<String, Integer> parseInt = Integer::parseInt;
// 等价于 s -> Integer.parseInt(s)

// 2. 绑定方法引用(特定对象的实例方法)
String str = "Hello";
Supplier<String> upper = str::toUpperCase;
// 等价于 () -> str.toUpperCase()

// 3. 非绑定方法引用(任意对象的实例方法)
Function<String, String> upperFn = String::toUpperCase;
// 等价于 s -> s.toUpperCase()

// 4. 构造器引用
Supplier<List<String>> listFactory = ArrayList::new;
// 等价于 () -> new ArrayList<String>()

Function<Integer, int[]> arrayFactory = int[]::new;
// 等价于 n -> new int[n]

5.1.4 Lambda 的捕获机制

Lambda 可以捕获外部的变量,但被捕获的变量必须是 effectively final 的:

java
int factor = 2;
Function<Integer, Integer> multiplier = n -> n * factor;
// factor 不能修改
// factor = 3;  // 编译错误

// 捕获的是值,不是引用
List<Runnable> tasks = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    tasks.add(() -> System.out.println(i));  // 每个 Lambda 捕获不同的 i 值
}

5.2 Stream API(Item 45-48)

5.2.1 Stream 的基本概念

Stream 是对集合操作的抽象,它允许你以声明式的方式处理数据:

java
// 命令式风格
List<String> result = new ArrayList<>();
for (String s : names) {
    if (s.length() > 3) {
        result.add(s.toUpperCase());
    }
}
Collections.sort(result);

// Stream 风格
List<String> result = names.stream()
    .filter(s -> s.length() > 3)
    .map(String::toUpperCase)
    .sorted()
    .collect(Collectors.toList());

5.2.2 Stream 的创建

java
// 从集合创建
List<String> list = List.of("a", "b", "c");
Stream<String> stream1 = list.stream();
Stream<String> parallelStream = list.parallelStream();

// 从数组创建
String[] array = {"a", "b", "c"};
Stream<String> stream2 = Arrays.stream(array);
Stream<String> stream3 = Stream.of(array);

// 生成 Stream
Stream<Integer> infinite = Stream.iterate(0, n -> n + 1);
Stream<Integer> finite = Stream.iterate(0, n -> n < 10, n -> n + 1);
Stream<Double> randoms = Stream.generate(Math::random);
Stream<Integer> range = IntStream.range(1, 10).boxed();

5.2.3 中间操作

java
Stream<String> stream = list.stream();

// 过滤
stream.filter(s -> s.length() > 3);

// 映射
stream.map(String::toUpperCase);
stream.flatMap(s -> Arrays.stream(s.split("")));

// 去重
stream.distinct();

// 排序
stream.sorted();
stream.sorted(Comparator.reverseOrder());

// 截取
stream.limit(10);
stream.skip(5);

// .peek() 调试
stream.peek(s -> System.out.println("Processing: " + s))
      .map(String::toUpperCase)
      .peek(s -> System.out.println("Mapped: " + s));

5.2.4 终端操作

java
// 收集
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
Map<String, Integer> map = stream.collect(
    Collectors.toMap(s -> s, String::length));

// 归约
Optional<String> longest = stream.reduce(
    (a, b) -> a.length() >= b.length() ? a : b);
String concat = stream.reduce("", (a, b) -> a + b);

// 统计
long count = stream.count();
OptionalInt max = stream.mapToInt(String::length).max();
IntSummaryStatistics stats = stream.collect(
    Collectors.summarizingInt(String::length));

// 匹配
boolean anyMatch = stream.anyMatch(s -> s.length() > 5);
boolean allMatch = stream.allMatch(s -> s.length() > 0);
boolean noneMatch = stream.noneMatch(s -> s.isEmpty());

// 查找
Optional<String> first = stream.findFirst();
Optional<String> any = stream.findAny();

// 遍历
stream.forEach(System.out::println);

5.2.5 Collector 的高级用法

java
// 分组
Map<Integer, List<String>> byLength = names.stream()
    .collect(Collectors.groupingBy(String::length));

// 分区
Map<Boolean, List<String>> partitioned = names.stream()
    .collect(Collectors.partitioningBy(s -> s.length() > 3));

// 多级分组
Map<Integer, Map<Character, List<String>>> grouped = names.stream()
    .collect(Collectors.groupingBy(
        String::length,
        Collectors.groupingBy(s -> s.charAt(0))
    ));

// 自定义下游收集器
Map<Integer, Long> countByLength = names.stream()
    .collect(Collectors.groupingBy(
        String::length,
        Collectors.counting()
    ));

// 连接字符串
String joined = names.stream()
    .collect(Collectors.joining(", ", "[", "]"));

// 映射后收集
Map<Integer, Set<String>> namesByLength = names.stream()
    .collect(Collectors.groupingBy(
        String::length,
        Collectors.mapping(String::toUpperCase, Collectors.toSet())
    ));

5.3 并行流(Item 48)

5.3.1 并行流的使用

java
// 串行流
long count = numbers.stream()
    .filter(n -> n % 2 == 0)
    .count();

// 并行流
long count = numbers.parallelStream()
    .filter(n -> n % 2 == 0)
    .count();

5.3.2 并行流的陷阱

数据竞争:如果 Lambda 修改了共享状态,并行流会导致数据竞争。

顺序依赖:如果操作依赖元素的顺序,并行流可能产生错误结果。

性能开销:对于小数据集或简单操作,并行流的开销可能大于收益。

底层数据结构:某些数据结构(如 LinkedList)不适合并行化。

java
// 反模式:并行流中的共享状态修改
List<String> result = new ArrayList<>();
names.parallelStream()
    .map(String::toUpperCase)
    .forEach(result::add);  // 数据竞争!

// 正确做法:使用 collect
List<String> result = names.parallelStream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());

三、实战案例

5.1 Stream 数据处理实战

java
public class SalesAnalytics {
    record Sale(String product, String category, double amount, LocalDate date) {}

    public static void analyze(List<Sale> sales) {
        // 1. 按类别统计总销售额
        Map<String, Double> totalByCategory = sales.stream()
            .collect(Collectors.groupingBy(
                Sale::category,
                Collectors.summingDouble(Sale::amount)
            ));

        // 2. 找出每个类别中销售额最高的产品
        Map<String, Optional<Sale>> topByCategory = sales.stream()
            .collect(Collectors.groupingBy(
                Sale::category,
                Collectors.maxBy(Comparator.comparingDouble(Sale::amount))
            ));

        // 3. 按月统计销售额趋势
        Map<YearMonth, Double> monthlyTrend = sales.stream()
            .collect(Collectors.groupingBy(
                sale -> YearMonth.from(sale.date()),
                TreeMap::new,
                Collectors.summingDouble(Sale::amount)
            ));

        // 4. 找出销售额前5的产品
        List<Sale> top5 = sales.stream()
            .sorted(Comparator.comparingDouble(Sale::amount).reversed())
            .limit(5)
            .collect(Collectors.toList());

        // 5. 统计各类别的销售笔数和平均金额
        Map<String, DoubleSummaryStatistics> stats = sales.stream()
            .collect(Collectors.groupingBy(
                Sale::category,
                Collectors.summarizingDouble(Sale::amount)
            ));

        // 6. 生成报告
        System.out.println("=== Sales Report ===");
        System.out.println("\nTotal by Category:");
        totalByCategory.forEach((cat, total) ->
            System.out.printf("  %s: $%.2f%n", cat, total));

        System.out.println("\nMonthly Trend:");
        monthlyTrend.forEach((month, total) ->
            System.out.printf("  %s: $%.2f%n", month, total));

        System.out.println("\nTop 5 Sales:");
        top5.forEach(sale ->
            System.out.printf("  %s (%s): $%.2f%n",
                sale.product(), sale.category(), sale.amount()));
    }
}

5.2 自定义 Collector 实战

java
// 自定义 Collector:收集最后 N 个元素
public class LastNCollector<T> implements Collector<T, LinkedList<T>, List<T>> {
    private final int n;

    public LastNCollector(int n) {
        this.n = n;
    }

    @Override
    public Supplier<LinkedList<T>> supplier() {
        return LinkedList::new;
    }

    @Override
    public BiConsumer<LinkedList<T>, T> accumulator() {
        return (list, element) -> {
            list.addLast(element);
            if (list.size() > n) {
                list.removeFirst();
            }
        };
    }

    @Override
    public BinaryOperator<LinkedList<T>> combiner() {
        return (left, right) -> {
            left.addAll(right);
            while (left.size() > n) {
                left.removeFirst();
            }
            return left;
        };
    }

    @Override
    public Function<LinkedList<T>, List<T>> finisher() {
        return LinkedList::new;
    }

    @Override
    public Set<Characteristics> characteristics() {
        return EnumSet.of(Characteristics.UNORDERED);
    }
}

// 使用
List<String> last3 = Stream.of("a", "b", "c", "d", "e")
    .collect(new LastNCollector<>(3));
System.out.println(last3);  // [c, d, e]

// 工厂方法
public static <T> Collector<T, ?, List<T>> lastN(int n) {
    return new LastNCollector<>(n);
}

四、本章小结

第5章系统讲解了 Lambda 和 Stream 的核心知识:

Lambda 表达式是函数式接口的实例,支持简洁的函数式编程。

方法引用是 Lambda 的简写形式,有四种类型。

Stream API 提供了声明式的数据处理方式。

Collector 是 Stream 终端操作的核心,支持丰富的收集和分组操作。

并行流可以提升性能,但需要注意数据竞争和顺序依赖。

Lambda 和 Stream 改变了 Java 的编程范式。正确使用它们,代码会变得更简洁、更具表达力。但也要警惕过度使用——不是所有场景都适合 Stream,有时传统的 for 循环更清晰、更高效。


五、思考与练习

思考题

Lambda 表达式的类型推断是如何工作的?

方法引用和 Lambda 在性能上有什么区别?

Stream 的惰性求值(lazy evaluation)是如何实现的?

并行流的 Fork/Join 框架是如何工作的?

如何选择合适的 Collector?

编程练习

实现一个文本分析工具:使用 Stream 统计文本中每个单词的出现次数,按频率排序输出。

实现一个数据管道:使用 Stream 实现 ETL(Extract-Transform-Load)管道,支持过滤、转换、聚合操作。

实现自定义 Collector:实现一个 toImmutableMap Collector,收集元素到不可变 Map 中。


5/10Lamb
0%