Java Lambda 表达式

函数式编程的优雅之道

让代码更简洁、更优雅、更强大
λ → 简洁代码

什么是 Lambda 表达式?

Lambda 表达式是 Java 8 引入的一个重要特性,它允许我们以更简洁的方式编写匿名函数。

传统写法 😰

// 对字符串列表进行排序
List<String> names = Arrays.asList("张三", "李四", "王五");
Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.compareTo(b);
    }
});

Lambda 写法 😍

// 同样的功能,更简洁
List<String> names = Arrays.asList("张三", "李四", "王五");
Collections.sort(names, (a, b) -> a.compareTo(b));

Lambda 表达式让代码更简洁、更易读、更函数式!

基本语法

基本语法结构

(参数列表) -> { 方法体 }
参数列表 可以为空,单个参数可省略括号
箭头操作符 分隔参数和方法体
方法体 单行可省略大括号和return

语法示例

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

// 单个参数(可省略括号)
x -> x * 2
(x) -> x * 2

// 多个参数
(x, y) -> x + y

// 多行方法体
(x, y) -> {
    int sum = x + y;
    return sum * 2;
}

函数式接口

Lambda 表达式只能用于函数式接口(只有一个抽象方法的接口)

自定义函数式接口

// 定义一个计算接口
interface Calculator {
    int calculate(int a, int b);
}

// 传统方式
Calculator add = new Calculator() {
    public int calculate(int a, int b) {
        return a + b;
    }
};

// Lambda 方式
Calculator add = (a, b) -> a + b;

Comparator

// 传统方式
Collections.sort(list, new Comparator<String>() {
    public int compare(String a, String b) {
        return a.compareTo(b);
    }
});

// Lambda 方式
Collections.sort(list, (a, b) -> a.compareTo(b));

关键点:这里创建的是对象,不是方法。

当你写 Calculator add = (a, b) -> a + b; 时,你做的是:

  • 定义了一个接口 (Calculator):它有一个抽象方法 calculate(int a, int b)
  • 创建了一个对象 (add):这个对象是 Calculator 接口的实例
  • Lambda 表达式:它实现了 calculate 方法的具体逻辑

所以 add 是一个对象,不是一个函数。调用时必须使用 add.calculate(3, 5),而不能直接用 add(3, 5)。

Lambda 表达式的限制

Lambda 表达式的核心优势是简化匿名内部类的写法,但它有一个重要前提:

Lambda 表达式只能用于只有一个抽象方法的接口。

这种接口被称为“函数式接口”(Functional Interface)。


如果接口有多个抽象方法会怎样?

如果一个接口定义了多个抽象方法,Java 编译器将无法推断 Lambda 表达式应该实现哪一个方法,因此会报错。

// 错误示例:接口不是函数式接口
interface MultiMethodInterface {
    void method1();
    void method2();
}

// 这行代码会编译失败
// MultiMethodInterface instance = () -> System.out.println("Hello"); 
// 编译器错误:The target type of this expression must be a functional interface

为了确保接口是函数式接口,可以使用 @FunctionalInterface 注解。如果接口不满足条件,编译器会报错。

常用函数式接口

1. Predicate<T> - 断言

功能:接受一个参数,返回 boolean 值。

核心方法:boolean test(T t)

// 定义判断条件:字符串是否为空
Predicate<String> isEmpty = s -> s.isEmpty();

// 使用 .test() 方法执行断言
System.out.println(isEmpty.test(""));        // true
System.out.println(isEmpty.test("hello"));   // false

2. Function<T, R> - 函数

功能:接受一个参数,返回一个结果。

核心方法:R apply(T t)

// 定义转换函数:获取字符串长度
Function<String, Integer> getLength = s -> s.length();

// 使用 .apply() 方法执行函数
System.out.println(getLength.apply("Hello"));    // 5
System.out.println(getLength.apply(""));         // 0

3. Consumer<T> - 消费

功能:接受一个参数,无返回值。

核心方法:void accept(T t)

// 定义消费操作:打印字符串
Consumer<String> print = s -> System.out.println(s);

// 使用 .accept() 方法执行操作
print.accept("Hello World");           // 输出: Hello World
print.accept("你好");                  // 输出: 你好

4. Supplier<T> - 供给

功能:无参数,返回一个结果。

核心方法:T get()

// 定义供应商:提供一个随机数
Supplier<Integer> randomNum = () -> (int)(Math.random() * 100);

// 使用 .get() 方法获取结果
System.out.println(randomNum.get());     // 输出一个 0-99 的随机数
System.out.println(randomNum.get());     // 再次调用,输出另一个随机数

基本应用

Lambda 表达式让集合操作变得更简洁、更优雅

简单应用示例

List<String> names = Arrays.asList("张三", "李四", "王五", "赵六");

// 传统方式:遍历并打印每个名字
for (String name : names) {
    System.out.println("姓名: " + name);
}

// Lambda 方式:更简洁的遍历
names.forEach(name -> System.out.println("姓名: " + name));

Lambda 的基本应用

  • forEach() - 遍历集合中的每个元素
  • sort() - 对集合进行排序
  • 自定义接口 - 创建自己的函数式接口
  • 方法引用 - 简化 Lambda 表达式

双参数场景:BiFunction

当函数需要两个参数时,可以使用 BiFunction<T, U, R> 接口,它接受两个参数并返回一个结果。

BiFunction 基本用法


// BiFunction<T, U, R>:T-第一个参数,U-第二个参数,R-返回值
BiFunction<Integer, Integer, Integer> adder = (a, b) -> a + b;
BiFunction<String, String, Boolean> isEqual = (s1, s2) -> s1.equals(s2);
BiFunction<Integer, String, String> repeat = (n, s) -> s.repeat(n);

// 使用 .apply() 方法调用
System.out.println(adder.apply(10, 20));        // 30
System.out.println(isEqual.apply("A", "B"));    // false
System.out.println(repeat.apply(3, "Hi"));      // HiHiHi

方法引用简介

当 Lambda 表达式只是调用一个现有方法时,可以使用方法引用来简化。它是 Lambda 的语法糖。


// Lambda 表达式
BiFunction<Integer, Integer, Integer> adder = (a, b) -> Math.addExact(a, b);

// 方法引用 - 使用 `::` 操作符
BiFunction<Integer, Integer, Integer> adderRef = Math::addExact;

// 两者完全等价,调用方式相同
System.out.println(adderRef.apply(10, 20)); // 30
                    

方法引用让代码更简洁,Math::addExact 等价于 (a, b) -> Math.addExact(a, b)。

实例方法引用的参数映射

实例方法引用有一个特殊的参数映射规则:函数式接口的第一个参数会成为方法的调用者。

参数映射规则

对于实例方法引用:

  • 函数式接口的第一个参数 → 方法的调用者(this)
  • 函数式接口的其余参数 → 方法的参数

示例1: String.equals()


// Lambda 表达式:两个参数
BiPredicate<String, String> checker = (s1, s2) -> s1.equals(s2);

// 实例方法引用:看起来没有参数
BiPredicate<String, String> checkerRef = String::equals;
                    

参数映射:

  • BiPredicate 的参数1 → String::equals 的调用者 (s1)
  • BiPredicate 的参数2 → String::equals 的方法参数 (s2)

示例2: String.startsWith()


// Lambda 表达式:两个参数
BiPredicate<String, String> starts = (str, prefix) -> str.startsWith(prefix);

// 实例方法引用
BiPredicate<String, String> startsRef = String::startsWith;
                    

参数映射:

  • BiPredicate 的参数1 → String::startsWith 的调用者 (str)
  • BiPredicate 的参数2 → String::startsWith 的方法参数 (prefix)

对比总结

方法引用类型 Lambda 表达式 方法引用 参数映射规则
静态方法引用 (a, b) -> Math.max(a, b) Math::max 参数一一对应
实例方法引用 (s1, s2) -> s1.equals(s2) String::equals 第一个参数成为调用者

更多参数场景

当需要三个或更多参数时,可以自定义函数式接口。


@FunctionalInterface
interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
}

// 使用自定义接口
TriFunction<Integer, Integer, Integer, Integer> sum3 = (a, b, c) -> a + b + c;
System.out.println(sum3.apply(1, 2, 3)); // 6

// 静态方法引用(如果存在)
// TriFunction<Integer, Integer, Integer, Integer> sum3Ref = MyMath::addThree;
                

这种模式可以扩展到任意数量的参数,只需定义相应的函数式接口即可。

方法引用的三种类型

方法引用是 Lambda 表达式的简化语法,根据引用的目标不同,分为三种类型

1. 静态方法引用

格式:ClassName::staticMethodName

// Lambda 表达式
Function<String, Integer> parser = s -> Integer.parseInt(s);
BiFunction<Integer, Integer, Integer> max = (a, b) -> Math.max(a, b);

// 方法引用
Function<String, Integer> parser = Integer::parseInt;
BiFunction<Integer, Integer, Integer> max = Math::max;

2. 实例方法引用

格式:object::instanceMethodName

// Lambda 表达式
Consumer<String> printer = s -> System.out.println(s);
Predicate<String> isEmpty = s -> s.isEmpty();

// 方法引用
Consumer<String> printer = System.out::println;
Predicate<String> isEmpty = String::isEmpty;

3. 构造器引用

格式:ClassName::new

// Lambda 表达式
Supplier<List<String>> listSupplier = () -> new ArrayList<>();
Function<String, Integer> intParser = s -> new Integer(s);

// 构造器引用
Supplier<List<String>> listSupplier = ArrayList::new;
Function<String, Integer> intParser = Integer::new;

注意:方法引用只是 Lambda 的语法糖,两者完全等价,选择哪种主要看代码简洁性和可读性。

实战应用

数字计算器应用

// 定义计算器接口
interface Calculator {
    int calculate(int a, int b);
}

// 使用 Lambda 表达式创建不同的计算器
Calculator add = (a, b) -> a + b;
Calculator subtract = (a, b) -> a - b;
Calculator multiply = (a, b) -> a * b;
Calculator divide = (a, b) -> a / b;

// 使用计算器
int result1 = add.calculate(10, 5);        // 15
int result2 = subtract.calculate(10, 5);   // 5
int result3 = multiply.calculate(10, 5);   // 50
int result4 = divide.calculate(10, 5);     // 2

// 对数字列表进行排序
List<Integer> numbers = Arrays.asList(5, 2, 8, 1, 9);
Collections.sort(numbers, (a, b) -> a.compareTo(b)); // 升序
Collections.sort(numbers, (a, b) -> b.compareTo(a)); // 降序

最佳实践

✅ 保持简洁

// 好的做法
list.forEach(System.out::println);

// 避免过于复杂的 Lambda
list.forEach(item -> {
    // 很多行代码...
    // 考虑提取为单独的方法
});

✅ 使用方法引用

// 推荐使用方法引用
names.forEach(System.out::println);

// 而不是
names.forEach(name -> System.out.println(name));

✅ 选择合适的场景

// 适合使用 Lambda:简单的操作
Collections.sort(numbers, (a, b) -> a.compareTo(b));

// 复杂逻辑建议使用传统方法
// 保持代码的可读性和可维护性

递归:函数式视角与 Java 的限制

函数式编程经常用递归表达“重复”。但 Java 的方法调用有栈深限制,且没有强制的尾递归优化,深递归可能导致 StackOverflowError。

递归版本(直观但有栈风险)

public class RecursionDemo {
    static long sum(int n) {
        if (n <= 0) return 0;
        return n + sum(n - 1);
    }

    public static void main(String[] args) {
        System.out.println(sum(10));
    }
}

迭代版本(更稳健)

public class IterationDemo {
    static long sum(int n) {
        long s = 0;
        for (int i = 1; i <= n; i++) s += i;
        return s;
    }
}

建议:在 Java 中,深递归优先改为迭代;递归更适合“树结构遍历/回溯”这类天然递归的问题。

闭包捕获:lambda vs 匿名内部类(有效 final)

Lambda 与匿名内部类都能“捕获”外部变量,但它们都要求捕获的局部变量是有效 final(Java 8+:不写 final 也行,但不能被重新赋值)。

可以捕获(有效 final)

import java.util.function.IntSupplier;

public class CaptureOk {
    static IntSupplier make() {
        int base = 10; // 没写 final,但不再修改 => 有效 final
        return () -> base + 1;
    }
}

不能捕获(会重新赋值)

import java.util.function.IntSupplier;

public class CaptureBad {
    static IntSupplier make() {
        int base = 10;
        base++; // 重新赋值后,就不再是有效 final
        return () -> base + 1; // 编译错误
    }
}

想要“可变状态”怎么办?常见做法是把状态放进对象里(例如数组/AtomicInteger/自定义 holder)。

相关知识在 [6-adv-2-inner-class.html](6-adv-2-inner-class.html) 也有专门讲解(局部内部类/匿名内部类对局部变量的访问规则)。

总结

🎯 Lambda 表达式的优势

  • 代码更简洁、更易读
  • 支持函数式编程范式
  • 让集合操作更优雅
  • 提高开发效率

🔑 关键概念

  • 函数式接口
  • Lambda 语法:(参数) -> { 方法体 }
  • 方法引用
  • 集合操作应用
  • 闭包捕获与有效 final

💡 学习建议

  • 多练习基本语法
  • 理解函数式编程思想
  • 在简单场景中应用
  • 关注代码的可读性

Lambda 表达式是现代 Java 开发的重要工具,掌握它将让你的代码更加优雅和高效!

1 / 12