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 表达式让代码更简洁、更易读、更函数式!
基本语法
基本语法结构
(参数列表) -> { 方法体 }
语法示例
// 无参数
() -> 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 开发的重要工具,掌握它将让你的代码更加优雅和高效!