1. 认识
简而言之:Lambda 表达式——是一种在“运行中”快速创建函数式接口实现的方式,无需声明单独的类或匿名类。它就像一个没有名字的迷你方法,可以作为参数传递或保存到变量中。
Java 很长一段时间都是一种“经典”的面向对象语言。如果你想传递一小段行为,例如按钮被按下时要做什么,就需要编写匿名类:
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick() {
System.out.println("按钮被点击!");
}
});
自 Java 8 起,出现了 Lambda 的“魔法”:
button.setOnClickListener(() -> System.out.println("按钮被点击!"));
这里的 () -> System.out.println("按钮被点击!") 就是一个 Lambda 表达式。
形式上:Lambda 表达式是对函数式接口中单个抽象方法实现的紧凑写法。
为什么这很重要?
- 可以把“行为”当作值传递(例如,处理列表中每个元素时做什么)。
- 编写更加简洁、可读性更高的代码。
- 使用 Java 的现代 API:Stream API、事件处理、异步任务等。
2. Lambda 表达式语法
通用形式
(parameters) -> expression
// 或者
(parameters) -> { 代码块 }
不同接口的示例
1. 无参数(例如 Runnable):
Runnable r = () -> System.out.println("你好,Lambda!");
r.run();
2. 单个参数(例如 Consumer):
Consumer<String> printer = s -> System.out.println("你传入了:" + s);
printer.accept("Java");
如果只有一个参数且其类型可推断,可以省略括号。
类型 String 是我们传入的变量(s)的类型。
3. 多个参数(例如 Comparator):
Comparator<Integer> cmp = (a, b) -> a - b;
System.out.println(cmp.compare(10, 5)); // 5
类型 Integer 是我们传入的变量(a、b)的类型。
4. 多行主体(若有显式返回,需要花括号并写 return):
Function<Integer, Integer> square = x -> {
int result = x * x;
return result;
};
System.out.println(square.apply(6)); // 36
第一个 Integer 是函数的返回类型,第二个 Integer 是参数类型。
简写与更简洁的写法
- 如果主体只有一个表达式,可以省略花括号和 return。
- 如果没有参数——写空括号:() -> ...
- 如果只有一个参数——可以省略括号:x -> ...
- 如果有多个参数——需要括号:(a,b) -> ...
汇总表
看看同一想法用 Lambda 和匿名类分别如何书写:
| 接口 | Lambda 表达式(示例) | 匿名类等价写法 |
|---|---|---|
|
|
|
|
|
|
|
|
|
自定义接口的示例
假设我们有一个函数式接口:
@FunctionalInterface
interface Operation {
int apply(int a, int b);
}
过去我们会这样实现:
Operation sum = new Operation() {
@Override
public void apply(int a, int b) {
return a + b;
}
};
现在——更现代:
Operation sum = (a, b) -> a + b;
System.out.println(sum.apply(3, 5)); // 8
标准接口的示例
Runnable:
Runnable hello = () -> System.out.println("Hello from thread!");
new Thread(hello).start();
Comparator:
List<String> list = Arrays.asList("苹果", "香蕉", "猕猴桃");
list.sort((a, b) -> a.length() - b.length());
System.out.println(list);
Function:
Function<String, Integer> parse = s -> Integer.parseInt(s);
System.out.println(parse.apply("123")); // 123
3. 作用域与变量捕获
外部上下文的变量(effectively final)
Lambda 表达式可以使用所在方法中的变量。但有一条规则:这些变量必须是effectively final——要么显式声明为 final,要么在初始化后不再被修改。
示例:
String prefix = "结果:";
Function<Integer, String> f = x -> prefix + (x * 2);
// prefix 在这里被“冻结”——之后不能再修改
System.out.println(f.apply(5)); // 结果:10
如果尝试在它被 Lambda 使用之后再修改 prefix,编译器会报错。
为什么?
Lambda 表达式可能会在声明它的那个方法返回之后才被调用。为避免“神奇”的 Bug,Java 只允许使用不可变的变量。
与匿名类的区别
在匿名类中也有同样的规则:外部方法的变量必须是 final/effectively final。但在作用域上有细微差别:匿名类内部的 this 指向匿名类本身,而在 Lambda 中——指向外部类。
public class Demo {
public void test() {
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println(this); // 输出:Demo$1(匿名类)
}
};
Runnable r2 = () -> System.out.println(this); // 输出:Demo(外部类)
r1.run();
r2.run();
}
}
Lambda 与类的字段
Lambda 表达式可以不受限制地访问外部类的字段:
public class Counter {
private int base = 10;
public void printSum(int x) {
Function<Integer, Integer> sum = y -> base + y + x;
System.out.println(sum.apply(5));
}
}
这里的 base——可以修改(它是类字段)。
x——必须是 effectively final。
4. 实战:写几个 Lambda 表达式
示例:过滤数字列表
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);
nums.stream()
.filter(n -> n % 2 == 0)
.forEach(n -> System.out.println("偶数:" + n));
关于 Stream API 的更多内容,你会在第 30 级了解 :P
示例:字符串转换函数
Function<String, String> capitalize = s -> s.toUpperCase();
System.out.println(capitalize.apply("java")); // JAVA
示例:自定义函数式接口
@FunctionalInterface
interface StringTransformer {
String transform(String s);
}
StringTransformer exclaim = s -> s + "!";
System.out.println(exclaim.transform("你好")); // 你好!
示例:使用外部上下文中的变量
int factor = 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n * factor));
// 之后不能再修改 factor!
5. 使用 Lambda 表达式时的常见错误
错误 1:试图修改被 Lambda 捕获的变量。
这样的代码无法编译——变量必须是 effectively final:
int sum = 0;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> sum += n); // 错误:sum 不是 final!
如果需要累加值——使用数组或包装对象。
错误 2:对 this 的作用域产生混淆。
在 Lambda 内部,this 指向外部类,而不是 Lambda 本身(与匿名类不同)。
错误 3:在多行 Lambda 中忘记花括号和 return。
如果 Lambda 主体不是单个表达式,就需要花括号和 return:
Function<Integer, Integer> square = x -> {
int y = x * x;
return y;
};
错误 4:错误地确定 Lambda 的类型。
Lambda 表达式始终是对某个函数式接口的实现。不能直接写:
var f = x -> x + 1; // 错误!无法知道是哪个接口类型。
需要显式指定类型:
Function<Integer, Integer> f = x -> x + 1;
GO TO FULL VERSION