Lambda表达式概述
Lambda表达式是 Java8 正式引入的一种语法,它的本质上是一个匿名函数,是对匿名函数的简写形式,它可以被传递给方法或存储在变量中,提供了一种清晰简洁的方式来表示一个接口的单个方法。
我们可以把 Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递),因为Lambda 表达式支持函数式编程范式,它允许开发人员传递行为而不是具体的值,这简化了代码结构和流程控制。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升;
而且它极大地增强了 Java 的编程模型,尤其在处理集合和流时,我们接下来在学习 Stream Api 和在 Spring 中异常处理与响应构建,Bean定义与条件配置等时候都需要用到 Lambda表达式
让我们通过一个具体例子看Lambda表达式如何简化代码:
传统匿名内部类写法:
1 | Comparator<String> comparator = new Comparator<String>() { |
Lambda表达式写法:
1 | Comparator<String> comparator = (s1, s2) -> s1.compareTo(s2); |
通过上面的对比,发现Lambda表达式式书写起来更为简洁;
lambda 表达式与 正则表达式(Regex)的区别
首先,这俩是两种完全不同的技术,用途和语法截然不同。
- 处理对象不同
- Lambda:处理行为逻辑(将代码作为数据传递,如函数作为参数);
- 正则:处理文本数据(定义字符串匹配规则)。
- 语法核心差异
- Lambda:依赖函数式接口,语法围绕 “参数→逻辑” 展开;
- 正则:依赖元字符组合,语法围绕 “字符模式”
展开(如
\w
匹配字母数字,*
表示零或多次重复)。
这俩的共同点也就在于均用于简化代码了,但解决的问题域却完全不同。
特性 | Lambda 表达式 | 正则表达式(Regex) |
---|---|---|
本质 | Java 语言特性(函数式编程) | 字符串匹配工具(文本模式) |
主要用途 | 简化匿名内部类,实现函数式接口 | 文本搜索、替换、验证(如邮箱、手机号格式) |
语法 | (参数) -> { 代码块 } |
由普通字符和特殊元字符(如\d 、* 、+ )组成的模式字符串 |
应用场景 | 集合操作、多线程、事件回调等 | 字符串处理、数据清洗、表单验证等 |
虽然二者用途不同,但可以配合使用,例如:
1 | List<String> emails = Arrays.asList( |
- Lambda:处理集合流(
stream().filter()
)。 - Regex:验证字符串格式(邮箱正则)。
二增
这个其实是不少初学者容易混淆的,因为Lambda
的->
和正则的某些转义符(如\\
)可能让初学者产生视觉混淆,而且两者都常用于简化代码,但本质作用完全不同;
Lambda表达式语法
基本语法
Lambda 表达式可以用来表示匿名函数,即一段没有声明的方法或者没有名字的代码片段。
基本的 Lambda 表达式语法如下:
1 | (参数列表) -> { 方法体 } |
- 参数列表:可以有零个或多个参数
- 无参数:
()
- 单个参数:
x
或(x)
- 多个参数:
(x, y, z)
- 参数类型可省略(Java 编译器通过上下文推断类型,如
(int x, int y)
可简写为(x, y)
)
- 无参数:
- 箭头操作符
->
分隔参数列表和方法体 - 方法体:可以是一个表达式或代码块
- 单行表达式:直接返回结果,无需
return
关键字 - 代码块:使用
{}
包裹多行代码,需要显式return
返回值
- 单行表达式:直接返回结果,无需
左边写的是实现的这个接口中的抽象方法中的形参列表,右边就是对抽象方法的处理;
1 | 实现的这个接口中的抽象方法中的形参列表 -> 抽象方法的处理 |
例如,对于一个简单的求和操作,可以这样表示:
1 | (int a, int b) -> a + b |
各种Lambda语法形式
无参数:
1 | () -> System.out.println("Hello World"); |
单个参数(可省略括号):
1 | x -> x * 2; // 等价于 (x) -> x * 2; |
多个参数:
1 | (x, y) -> x + y; |
复杂方法体(需要大括号和return语句):
1 | (x, y) -> { |
无返回值有形参的抽象方法:
1 | public class MyTest1 { |
- 可以省略方法名,IDEA会帮你自动检测方法名;
- 可以省略方法中的形参类型;
- 如果对抽象方法的实现逻辑只有一行,可以省略方法体的大括号,当然如果不止一行,就不能省略了;
有返回值的抽象方法:
1 | public class MyTest2 { |
- 有返回值的方法,如果要去掉大括号,还需要去掉return关键字;
- 形参列表中只有一个参数,可以去掉形参的括号;
基本用法
假设我们有一个函数接口 Calculator
:
1 |
|
我们可以使用 Lambda 表达式为这个接口定义行为:
1 | public class LambdaBasic { |
Lambda 表达式必须与函数式接口配合使用,且表达式的参数列表和返回值必须与接口中抽象方法的签名一致。例如:
1 | // 函数式接口:接收 String,返回 int |
Lambda 表达式可以访问外部变量,但有严格的限制:
访问局部变量:局部变量必须是最终的(final)或事实上的最终变量(即赋值后不再修改)。
1
2
3
4
5
6int factor = 2; // 事实上的最终变量(未显式声明 final,但未修改)
Calculator multiply = (a, b) -> (a + b) * factor;
System.out.println(multiply.calculate(3, 4)); // 输出:14((3+4)*2)
// 错误示例:修改局部变量会导致编译失败
factor = 3; // 编译报错:Lambda 表达式中引用的局部变量必须是最终的或事实上的最终变量访问成员变量和静态变量:无限制,可修改。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class LambdaScope {
private int instanceVar = 10;
private static int staticVar = 100;
public void test() {
// 访问实例变量并修改
Calculator accessInstance = (a, b) -> {
instanceVar = 20;
return a + b + instanceVar;
};
// 访问静态变量并修改
Calculator accessStatic = (a, b) -> {
staticVar = 200;
return a + b + staticVar;
};
}
}
而且Lambda表达式可以作为参数进行传递
1 | import java.util.Arrays; |
而且Lambda 表达式在集合操作、多线程等场景中应用广泛,例如:
- 集合遍历(替代匿名内部类)
1 | List<String> fruits = Arrays.asList("Apple", "Banana", "Orange"); |
线程创建
1
2
3
4
5
6
7
8
9
10// 传统方式:匿名 Runnable
Thread thread1 = new Thread(new Runnable() {
public void run() {
System.out.println("Thread 1 running");
}
});
// Lambda 简化
Thread thread2 = new Thread(() -> System.out.println("Thread 2 running"));
Lambda表达式并非万能
Lambda表达式不是万能的,他需要函数式接口的支持;
什么是函数式接口
函数式接口的核心定义是:只包含一个抽象方法的接口(可以包含默认方法、静态方法或从Object
继承的方法)。
函数式接口存在如下关键特征:
抽象方法数量严格为 1(若接口中声明的抽象方法与
Object
类的方法重名,该方法不计入 “抽象方法数量”,因为所有类都继承自Object
,本质上不算接口新增的抽象方法)。1
2
3
4
5
6// 合法的函数式接口:抽象方法仅1个(toString()来自Object,不计入)
interface MyInterface {
void doSomething(); // 唯一抽象方法
String toString(); // 继承自Object,不算新增抽象方法
}可通过
@FunctionalInterface
注解显式标记,编译器会自动验证接口是否符合函数式接口规范(若不符合则报错),自定义函数式接口也需要如上注解1
2
3
4
5
6// 错误示例:包含2个抽象方法,编译报错
interface InvalidInterface {
void method1();
void method2(); // 编译器报错:函数式接口只能有一个抽象方法
}此外,函数式接口可以继承其他接口,但需保证最终抽象方法数量仍为 1:
1
2
3
4
5
6
7
8
9
10
11// 父接口(函数式接口)
interface Parent {
void parentMethod();
}
// 子接口:继承父接口,且不新增抽象方法(仍为函数式接口)
interface Child extends Parent {
default void childMethod() { ... } // 默认方法,不影响抽象方法数量
}
Lambda 表达式的本质是函数式接口抽象方法的匿名实现,二者必须满足 “方法签名兼容”:
- Lambda 的参数列表类型、数量、顺序需与接口抽象方法一致;
- Lambda 的返回值类型需与接口抽象方法兼容(若接口方法有返回值,Lambda 的返回值需可赋值给该类型)。
1 | // 函数式接口 |
为避免重复定义通用的函数式接口,JDK 1.8
在java.util.function
包中提供了一系列内置函数式接口,覆盖大多数常见场景。掌握这些接口可以显著减少代码量
接口名称 | 抽象方法 | 功能描述 | 示例场景 |
---|---|---|---|
Consumer<T> |
void accept(T t) |
接收一个参数,无返回值 | 集合遍历(forEach ) |
Supplier<T> |
T get() |
无参数,返回一个值 | 数据生成(如工厂方法) |
Function<T, R> |
R apply(T t) |
接收 T 类型参数,返回 R 类型值 | 数据转换(如类型转换) |
Predicate<T> |
boolean test(T t) |
接收参数,返回布尔值 | 条件判断(如过滤集合) |
BiFunction<T,U,R> |
R apply(T t, U u) |
接收两个参数,返回一个值 | 多参数转换(如计算) |
那么什么时候不适用 Lambda 表达式
若接口包含多个抽象方法(非函数式接口),Lambda 无法使用,此时必须通过匿名内部类或显式实现类
1 | // 非函数式接口(2个抽象方法) |
Lambda 表达式本质是 “匿名函数”,无法像普通类一样通过成员变量保留状态,也无法直接复用(需通过变量引用间接复用)。若需频繁复用复杂逻辑,建议定义普通类或枚举。
方法引用
方法引用与 Lambda 表达式
什么是方法引用,他和 Lambda 表达式什么关系
方法引用是 Java 8 中与 Lambda 表达式紧密关联的特性,它可以看作是 Lambda 表达式的 “语法糖”—— 当 Lambda 表达式的逻辑只是调用一个已存在的方法时,方法引用能以更简洁的方式表达这种逻辑。
方法引用的核心思想是:如果 Lambda 表达式的实现仅仅是调用一个已有的方法(无需额外逻辑),则可以直接通过方法名引用该方法,替代 Lambda 表达式。
- 方法引用是 Lambda 表达式的简化形式。例如,Lambda 表达式
(a, b) -> Math.max(a, b)
可以简化为方法引用Math::max
。 二者本质上是一致的,都会被编译为函数式接口的实现,但方法引用的语法更简洁,可读性更强。 - 方法引用不会创建新方法:它只是对已有方法的 “引用”,不会改变原方法的行为。
方法引用通过 ::
运算符连接类名(或对象名)与方法名,语法分为以下 4 种常见形式:
方法引用类型 | 语法格式 | 示例 | 对应的 Lambda 表达式 |
---|---|---|---|
静态方法引用 | 类名::静态方法名 | Math::max |
(a, b) -> Math.max(a, b) |
实例方法引用(对象) | 对象名::实例方法名 | str::length |
() -> str.length() |
实例方法引用(类) | 类名::实例方法名 | String::compareTo |
(s1, s2) -> s1.compareTo(s2) |
构造方法引用 | 类名::new | ArrayList::new |
() -> new ArrayList<>() |
- 静态方法引用通过 “类名::方法名” 调用,不依赖实例;
- 实例方法引用(对象名::方法名)依赖具体实例,若实例为
null
,调用时会抛出NullPointerException
。 - 构造方法引用中,当类有多个构造方法时,JVM 会根据函数式接口的抽象方法签名自动匹配最合适的构造方法。若匹配失败,会编译报错。
静态方法引用(类名::静态方法名)
当 Lambda 表达式调用的是某个类的静态方法时,可使用静态方法引用。
1 | import java.util.function.BiFunction; |
实例方法引用(对象名::实例方法名)
当 Lambda 表达式调用的是某个对象的实例方法时,可使用实例方法引用(需先创建对象)。
1 | import java.util.function.Supplier; |
实例方法引用(类名::实例方法名)
当 Lambda 表达式的第一个参数是方法的调用者,后续参数是方法的参数时,可使用类名引用实例方法。
此时方法的调用者是 Lambda 表达式的第一个参数,因此可以省略参数名,直接通过类名引用方法。
1 | import java.util.function.BiFunction; |
构造方法引用(类名::new)
当 Lambda 表达式的逻辑是创建一个对象(即调用构造方法)时,可使用构造方法引用
构造方法引用会根据函数式接口的抽象方法签名,自动匹配对应的构造方法(如无参、单参、多参)。
1 | import java.util.function.Supplier; |
方法引用并非在所有场景下都能替代 Lambda 表达式,它需要满足以下条件:
方法参数与返回值匹配:被引用的方法的参数列表和返回值类型,必须与函数式接口中抽象方法的参数列表和返回值类型完全一致。 例如,
Math.max(int a, int b)
的参数是两个int
,返回int
,因此可匹配BiFunction<Integer, Integer, Integer>
接口(参数为两个Integer
,返回Integer
)。Lambda 逻辑仅为方法调用:若 Lambda 表达式中除了调用方法还有其他逻辑(如条件判断、循环),则不能使用方法引用。
1
2
3
4
5// 错误:Lambda 包含额外逻辑,无法用方法引用替代
Function<Integer, Integer> invalidRef = num -> {
System.out.println("处理中..."); // 额外逻辑
return Math.abs(num);
};