背景
本文是《Java 后端从小白到大神》修仙系列第十三篇,正式进入Java后端世界,本篇文章主要聊Java基础中的Lambda表达式。若想详细学习请点击首篇博文,我们开始吧。
文章概览
- Lambda表达式概述
- Lambda表达式语法
- 函数式接口
- Lambda表达式的使用技巧
- Lambda表达式的实际应用
- Lambda表达式的原理
- Lambda表达式最佳实践
- 常见问题与解决方案
1. Lambda表达式概述
Lambda 表达式,就是 Java 8 用来简写代码的语法。专门用来快速实现只有一个抽象方法的接口,省去写类、写匿名内部类的麻烦,让代码更短、更清晰。一句话总结:Lambda = 简化版的匿名方法,只针对函数式接口。
1.1 核心特性
- 简洁性:替代冗余的
匿名内部类,使代码更简洁易读
- 函数式编程:支持将函数作为参数传递,实现函数式编程风格
- 类型推断:编译器自动推断参数类型,减少代码冗余
- 闭包:可以访问外部的
final或等效final的局部变量
1.2 Lambda表达式总结
- 函数式接口 = 只有1个抽象方法的接口
- Lambda = 快速实现这个方法
- Lambda 的格式必须和接口里的方法匹配
- Runnable / Consumer / Function 都是内置的函数式接口
- 它们代表不同的函数类型(有无参数、有无返回)
- 函数式接口类型格式:函数式接口<参数类型(1 个或多个),返回值类型>
Lambda 是实现,函数式接口是它的类型!
生活场景
函数式接口 = 插座,Lambda = 插头
- Runnable 是两孔插座
- Consumer 是三孔插座
- Function 是带接地的插座
Lambda 必须形状对应,才能插进去!
2. Lambda表达式语法
2.1 基本语法
- 参数列表:可以是空参数、单参数或多参数,类型可省略(编译器自动推断)
- 箭头符号:
-> 分隔参数和代码体
- 代码体:单行代码可省略
{} 和 return,多行需明确写 {} 和 return
2.2 语法变体
| 场景 |
Lambda 写法 |
说明 |
| 无参数,无返回值 |
() -> System.out.println("Hello") |
适用于 Runnable 等接口 |
| 单参数,无类型 |
s -> System.out.println(s) |
编译器自动推断参数类型 |
| 单参数,带类型 |
(String s) -> System.out.println(s) |
显式指定参数类型 |
| 多参数,带类型 |
(int a, int b) -> a + b |
多个参数需要使用括号 |
| 多行代码体 |
(a, b) -> { int c = a + b; return c; } |
多行代码需要使用大括号和 return 语句 |
| 方法引用 |
System.out::println |
当 Lambda 只是调用一个已有的方法时使用 |
2.3 语法示例
Java自带的函数式接口
Runnable / Consumer / Function 全部都是 函数式接口!
- Runnable:规定无参、无返回
- Consumer:规定1个参、无返回
- Function:规定1个参、1个返回
Runnable 接口长这样(内部)
1
2
3
4
|
@FunctionalInterface
public interface Runnable {
void run(); // 只有 1 个抽象方法:无参、无返回
}
|
所以 Lambda 必须匹配它
1
|
() -> System.out.println("...")
|
完全对应:无参、无返回
Consumer 接口长这样
1
2
3
4
|
@FunctionalInterface
public interface Consumer<T> {
void accept(T t); // 1个参、无返回
}
|
Lambda 必须匹配
1
|
s -> System.out.println(s)
|
完全对应:1个参、无返回
Function 接口长这样
1
2
3
4
|
@FunctionalInterface
public interface Function<T,R> {
R apply(T t); // 1个参、1个返回
}
|
Lambda 匹配
完全对应:1个参、1个返回
匿名内部类格式
Java 就把这一整块 → 当成一个「临时的、没有名字的类」
1
2
3
|
new 接口/父类() {
只要这里写方法 = 这就是类
}
|
代码示例
无参无返回 → Runnable
1
|
Runnable runnable = () -> System.out.println("Hello");
|
1参无返回 → Consumer
1
|
Consumer<String> consumer = s -> System.out.println(s);
|
1参有返回 → Function
1
|
Function<Integer, Integer> square = x -> x * x;
|
2参有返回 → BiFunction
1
2
3
4
|
// BiFunction<Integer, Integer, String>
// ↑ ↑ ↑
// 参数1 参数2 返回值类型
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
|
多行代码体
1
2
3
4
5
6
7
8
9
|
Function<Integer, String> converter = (num) -> {
if (num < 0) {
return "负数";
} else if (num == 0) {
return "零";
} else {
return "正数";
}
};
|
完整代码(不用 Lambda)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class Test {
public static void main(String[] args) {
// 1. 正常写法:创建 Runnable 对象(匿名内部类)
Runnable runnable = new Runnable() {
@Override
public void run() {
// 这里是要执行的代码
System.out.println("我是 run() 方法里的代码");
}
};
// 2. 正常调用:直接调用 run() 方法
runnable.run();
}
}
|
换成 Lambda 后
1
2
3
4
5
6
7
8
9
10
|
public class Test {
public static void main(String[] args) {
// Lambda 简化版
Runnable runnable = () -> System.out.println("我是 run() 方法里的代码");
// 调用方式 完全一样!
runnable.run();
}
}
|
3. 函数式接口
3.1 什么是函数式接口
函数式接口(Functional Interface)是 Java 中只包含一个抽象方法的接口。从 Java 8 开始,函数式接口可以使用 Lambda 表达式进行实例化,从而简化代码编写。
特点:
- 只有一个抽象方法(可以有多个默认方法和静态方法)
- 可以使用
@FunctionalInterface 注解标记(推荐使用,确保编译器检查)
3.2 内置函数式接口
Java 8 提供了一些常用的内置函数式接口,位于 java.util.function 包中:
| 接口 |
方法签名 |
用途 |
Runnable |
void run() |
无参数,无返回值的任务 |
Callable<V> |
V call() |
无参数,带返回值的任务,支持抛出异常 |
Consumer<T> |
void accept(T) |
接受单个输入参数,无返回值 |
Supplier<T> |
T get() |
无参数,返回结果 |
Function<T, R> |
R apply(T) |
接受单个输入参数,返回结果 |
Predicate<T> |
boolean test(T) |
接受单个输入参数,返回布尔值,用于条件判断 |
BiFunction<T, U, R> |
R apply(T, U) |
接受两个输入参数,返回结果 |
UnaryOperator<T> |
T apply(T) |
接受一个参数,返回相同类型的结果 |
BinaryOperator<T> |
T apply(T, T) |
接受两个相同类型的参数,返回相同类型的结果 |
3.3 自定义函数式接口
自定义一个函数式接口,只需创建一个接口并在其中声明一个抽象方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
@FunctionalInterface
interface MathOperation {
int operate(int a, int b);
// 可以有默认方法
default String getOperationName() {
return "数学运算";
}
// 可以有静态方法
static MathOperation add() {
return (a, b) -> a + b;
}
}
// 使用示例
public class CustomFunctionalInterfaceExample {
public static void main(String[] args) {
// 使用 Lambda 表达式实现自定义函数式接口
MathOperation addition = (a, b) -> a + b;
MathOperation subtraction = (a, b) -> a - b;
System.out.println("加法: " + operate(10, 5, addition));
System.out.println("减法: " + operate(10, 5, subtraction));
System.out.println("操作名称: " + addition.getOperationName());
// 使用静态方法创建实例
MathOperation staticAdd = MathOperation.add();
System.out.println("静态方法加法: " + operate(20, 10, staticAdd));
}
public static int operate(int a, int b, MathOperation operation) {
return operation.operate(a, b);
}
}
|
4. Lambda表达式的使用技巧
4.1 方法引用(Method Reference)
当 Lambda 体里,只做了一件事:调用一个已经存在的方法,别的啥也没干 → 就可以用方法引用 :: 简化。
看这个 Lambda:
1
|
s -> System.out.println(s)
|
它干了什么?
只干了一件事:调用了已经存在的 println 方法
没有计算、没有判断、没有新逻辑,就只是转发调用。
这种就叫:
Lambda 只是调用一个已有的方法
于是可以简化成方法引用
效果完全一样,但更短。
静态方法引用
1
2
3
4
5
|
// Lambda:只调用 Math.abs
a -> Math.abs(a)
// 方法引用
Math::abs
|
实例方法引用
1
2
3
4
5
6
7
|
String str = "hello";
// Lambda:只调用 str.length()
() -> str.length()
// 方法引用
str::length
|
构造方法引用
1
2
3
4
5
|
// Lambda:只 new 对象
() -> new String()
// 方法引用
String::new
|
什么时候不能用方法引用?
只要 Lambda 里多干了一点事,就不能用:
1
2
3
4
|
s -> {
System.out.println(s);
System.out.println("额外一句"); // 多干了 → 不能简化
}
|
1
|
a -> Math.abs(a) * 2; // 计算了 → 不能简化
|
Lambda 里只做一件事:调用现成方法
→ 就能写成 类名::方法 或 对象::方法
4.2 Lambda 的变量捕获
什么是 Lambda 的变量捕获?Lambda 表达式 可以使用方法里定义的普通局部变量,就像把外面的变量“抓进来”用,这就叫 变量捕获。
规则只有一句话:
被 Lambda 抓到里面用的局部变量,必须是 不会变的(有效 final)。
也就是说:
- 变量定义后就再也没改过值
- 虽然你没写
final 关键字
- 但编译器看它行为就是“不变的”
→ 这就叫 有效 final(effectively final)
举个最直观的例子
可以用(有效 final)
1
2
3
4
5
6
7
|
public void test() {
int num = 10; // 只赋值一次,后面没改过
Runnable r = () -> {
System.out.println(num); // 可以捕获
};
}
|
不可以用(不是有效 final)
1
2
3
4
5
6
7
8
|
public void test() {
int num = 10;
num = 20; // 变量被修改了!
Runnable r = () -> {
System.out.println(num); // 报错!
};
}
|
为什么必须是“不变的”?
-
Lambda 不是立刻执行,可能延后执行
它会把变量复制一份自己用。
如果原变量后来变了,两边不一致就会混乱。
-
避免多线程下出 bug
多个线程同时跑 Lambda,变量值一变,结果就不可预测。
-
编译器好处理
不变的东西,编译器能直接锁死值,不用时刻跟踪变化。
总结
- 变量捕获 = Lambda 用了外面方法里的局部变量
- 有效 final = 这个变量只赋值一次,后面再也不改
- 规则:Lambda 只能抓不变的局部变量,改过就报错
在 Lambda 中使用可变变量的解决方案:
- Lambda 不能改外部局部变量,但可以改对象里的字段:
1
2
3
4
5
|
// 自定义 Counter 类
Counter counter = new Counter(0);
nums.forEach(n -> {
counter.value++; // 允许!
});
|
- 使用数组:
1
2
|
int[] count = {0};
nums.forEach(n -> count[0]++);
|
- 使用原子类:
1
2
|
AtomicInteger count = new AtomicInteger(0);
nums.forEach(n -> count.incrementAndGet());
|
4.3 结合 Stream API
Lambda 表达式与 Stream API 配合使用,提供了强大的集合操作能力:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 过滤偶数并计算平方和
int sumOfEvenSquares = numbers.stream()
.filter(n -> n % 2 == 0) // 过滤偶数
.map(n -> n * n) // 计算平方
.mapToInt(Integer::intValue) // 转换为 int 流
.sum(); // 求和
System.out.println("偶数平方和: " + sumOfEvenSquares); // 输出 220
// 分组统计
Map<String, List<Integer>> groupedByParity = numbers.stream()
.collect(Collectors.groupingBy(n -> n % 2 == 0 ? "偶数" : "奇数"));
System.out.println("分组统计: " + groupedByParity);
// 输出: {奇数=[1, 3, 5, 7, 9], 偶数=[2, 4, 6, 8, 10]}
|
5. Lambda表达式的实际应用
5.1 替代匿名内部类
线程创建:
1
2
3
4
5
6
7
8
9
10
|
// 传统写法
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程运行中");
}
}).start();
// Lambda 写法
new Thread(() -> System.out.println("线程运行中")).start();
|
集合排序:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
List<String> names = Arrays.asList("Bob", "Alice", "Tom", "David");
// 传统写法
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
});
// Lambda 写法
names.sort((s1, s2) -> s1.compareTo(s2));
// 方法引用写法
names.sort(String::compareTo);
|
5.2 事件驱动编程
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// GUI 事件监听(示例)
button.addActionListener(e -> System.out.println("按钮被点击!"));
// 鼠标事件监听(示例)
panel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
System.out.println("鼠标点击: " + e.getPoint());
}
});
// 注意:MouseAdapter 不是函数式接口,因为它有多个方法
// 但可以使用 Lambda 表达式配合函数式接口使用
|
5.3 并行处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class StreamTest {
public static void main(String[] args) {
// 生成 1 ~ 1000000 的数字列表
List<Integer> numbers = IntStream.rangeClosed(1, 1000000)
.boxed()
.collect(Collectors.toList());
// ===================== 串行流:一个人按顺序干活 =====================
// 记录开始时间
long startTime = System.currentTimeMillis();
// 串行计算:单线程逐个处理,筛选偶数并求和
int sumSerial = numbers.stream() // 串行流:单线程处理
.filter(n -> n % 2 == 0) // 只保留偶数
.mapToInt(Integer::intValue)
.sum(); // 求和
// 串行耗时
long serialTime = System.currentTimeMillis() - startTime;
// ===================== 并行流:多个人同时一起干活 =====================
// 重新记录开始时间
startTime = System.currentTimeMillis();
// 并行计算:多线程同时处理,速度更快
int sumParallel = numbers.parallelStream() // 并行流:多线程同时处理
.filter(n -> n % 2 == 0) // 只保留偶数
.mapToInt(Integer::intValue)
.sum(); // 求和
// 并行耗时
long parallelTime = System.currentTimeMillis() - startTime;
// 输出结果:结果一定一样,时间并行通常更短
System.out.println("串行处理结果: " + sumSerial + ", 时间: " + serialTime + "ms");
System.out.println("并行处理结果: " + sumParallel + ", 时间: " + parallelTime + "ms");
}
}
|
5.4 函数式接口参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class Calculator {
public static void main(String[] args) {
// 使用函数式接口作为参数
int result1 = calculate(10, 5, (a, b) -> a + b);
int result2 = calculate(10, 5, (a, b) -> a - b);
int result3 = calculate(10, 5, (a, b) -> a * b);
int result4 = calculate(10, 5, (a, b) -> a / b);
System.out.println("加法: " + result1);
System.out.println("减法: " + result2);
System.out.println("乘法: " + result3);
System.out.println("除法: " + result4);
}
public static int calculate(int a, int b, BiFunction<Integer, Integer, Integer> operation) {
return operation.apply(a, b);
}
}
|
6. Lambda表达式的原理
6.1 工作机制
Lambda 表达式在运行时会被编译为以下形式之一:
- 静态方法:如果 Lambda 表达式没有捕获外部变量
- 实例方法:如果 Lambda 表达式捕获了外部变量
- 匿名内部类:在某些复杂情况下
6.2 类型推断
编译器通过上下文推断 Lambda 表达式的类型:
- 从函数式接口的抽象方法签名推断参数类型
- 从代码体推断返回类型
6.3 闭包实现
什么是闭包? Lambda 能“记住并使用”它外面的变量,这个能力就叫闭包。 就这么简单。
用最直白的例子讲闭包
1
2
3
4
5
6
7
|
public void test() {
int age = 20; // 外部变量
Runnable r = () -> {
System.out.println(age); // Lambda 用到了外面的 age
};
}
|
这里:
- Lambda 用到了外部变量 age
- Lambda 把 age “抓进来、记住它”
- 这个行为 → 闭包
7. Lambda表达式最佳实践
7.1 代码风格
- 保持简洁:Lambda 表达式应该简短明了,避免复杂逻辑
- 使用方法引用:当 Lambda 只是调用一个已有的方法时,使用方法引用
- 合理命名参数:使用有意义的参数名,提高代码可读性
- 避免过长的 Lambda:如果 Lambda 表达式超过 3-4 行,考虑将其提取为方法
7.2 性能考虑
- 避免在循环中创建 Lambda:重复创建 Lambda 表达式会影响性能
- 合理使用并行流:对于大型集合,并行流可以提高性能,但对于小型集合可能会有 overhead
- 注意自动装箱:在处理基本类型时,注意避免频繁的自动装箱和拆箱
7.3 常见模式
1. 策略模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// 传统策略模式
interface PaymentStrategy {
void pay(int amount);
}
class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("信用卡支付: " + amount);
}
}
// 使用 Lambda 表达式
PaymentStrategy creditCardPayment = amount -> System.out.println("信用卡支付: " + amount);
PaymentStrategy wechatPayment = amount -> System.out.println("微信支付: " + amount);
// 使用
creditCardPayment.pay(100);
wechatPayment.pay(200);
|
2. 模板方法模式:
1
2
3
4
5
6
7
8
9
10
|
// 模板方法
public void processTemplate(List<Integer> numbers, Function<Integer, Integer> processor) {
for (Integer number : numbers) {
System.out.println(processor.apply(number));
}
}
// 使用
processTemplate(Arrays.asList(1, 2, 3), n -> n * n); // 平方
processTemplate(Arrays.asList(1, 2, 3), n -> n * 2); // 翻倍
|
8. 常见问题与解决方案
8.1 变量捕获问题
问题:Lambda 表达式中不能修改外部变量
解决方案:使用类、原子类或数组来存储可变值
8.2 类型推断问题
问题:编译器无法推断 Lambda 表达式的类型
解决方案:显式指定参数类型或使用类型转换
1
2
3
4
5
6
7
8
|
// 类型推断失败
Function<Object, String> converter = obj -> obj.toString(); // 编译错误
// 解决方案 1:显式指定参数类型
Function<Object, String> converter1 = (Object obj) -> obj.toString();
// 解决方案 2:使用类型转换
Function<Object, String> converter2 = obj -> ((String) obj).toUpperCase();
|
8.3 异常处理问题
问题:Lambda 表达式中抛出的检查型异常需要处理
解决方案:在 Lambda 表达式内部处理异常或使用包装方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
public class LambdaExceptionDemo {
public static void main(String[] args) {
List<String> files = Arrays.asList("file1.txt", "file2.txt");
// ==============================================
// 【错误写法】
// Lambda 里直接抛检查型异常(IOException),编译报错!
// 因为 forEach 接收的 Consumer 接口没声明 throws Exception
// ==============================================
/*
files.forEach(file -> {
// 编译报错:未处理的异常 java.io.IOException
Files.readAllLines(Paths.get(file));
});
*/
// ==============================================
// 【方案1:最常用】在 Lambda 内部 try-catch 处理
// ==============================================
files.forEach(file -> {
try {
Files.readAllLines(Paths.get(file));
} catch (IOException e) {
System.err.println("读取文件失败:" + file);
e.printStackTrace();
}
});
// ==============================================
// 【方案2:优雅写法】包装成运行时异常
// 定义一个允许抛异常的函数式接口,再统一包装
// ==============================================
files.forEach(fileWrapper(file -> {
// 这里可以 throws IOException,外部无感
Files.readAllLines(Paths.get(file));
}));
}
// ==============================================
// 包装工具方法:把“会抛受检异常”的Lambda,转成普通Consumer
// ==============================================
public static Consumer<String> fileWrapper(ThrowingConsumer<String> throwingConsumer) {
return file -> {
try {
throwingConsumer.accept(file);
} catch (Exception e) {
// 捕获受检异常,包装成运行时异常抛出
throw new RuntimeException("文件处理异常", e);
}
};
}
// ==============================================
// 自定义函数式接口:允许抛出 Exception
// ==============================================
@FunctionalInterface
public interface ThrowingConsumer<T> {
void accept(T t) throws Exception;
}
}
|
总结
Lambda 表达式是 Java 8 引入的重要特性,它不仅简化了代码,还为 Java 带来了函数式编程的能力。应该掌握以下内容:
- Lambda 表达式的语法:基本语法和各种变体
- 函数式接口:内置函数式接口和自定义函数式接口
- 使用技巧:方法引用、变量捕获、结合 Stream API
- 实际应用:替代匿名内部类、事件驱动编程、并行处理
- 原理:工作机制、类型推断、闭包实现
- 最佳实践:代码风格、性能考虑、常见模式
- 问题解决:变量捕获、类型推断、异常处理
Lambda 表达式特别适合集合操作、异步任务和事件处理等场景,熟练掌握 Lambda 表达式可以使你的代码更加简洁、优雅和高效。在实际开发中,应该根据具体场景合理使用 Lambda 表达式,充分发挥其优势。