Lambda表达式概述

Lambda表达式是 Java8 正式引入的一种语法,它的本质上是一个匿名函数,是对匿名函数的简写形式,它可以被传递给方法或存储在变量中,提供了一种清晰简洁的方式来表示一个接口的单个方法。

我们可以把 Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递),因为Lambda 表达式支持函数式编程范式,它允许开发人员传递行为而不是具体的值,这简化了代码结构和流程控制。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升;

而且它极大地增强了 Java 的编程模型,尤其在处理集合和流时,我们接下来在学习 Stream Api 和在 Spring 中异常处理与响应构建,Bean定义与条件配置等时候都需要用到 Lambda表达式

让我们通过一个具体例子看Lambda表达式如何简化代码:

传统匿名内部类写法:

1
2
3
4
5
6
Comparator<String> comparator = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
};

Lambda表达式写法:

1
Comparator<String> comparator = (s1, s2) -> s1.compareTo(s2);

通过上面的对比,发现Lambda表达式式书写起来更为简洁;

lambda 表达式与 正则表达式(Regex)的区别

首先,这俩是两种完全不同的技术,用途和语法截然不同。

  • 处理对象不同
    • Lambda:处理行为逻辑(将代码作为数据传递,如函数作为参数);
    • 正则:处理文本数据(定义字符串匹配规则)。
  • 语法核心差异
    • Lambda:依赖函数式接口,语法围绕 “参数→逻辑” 展开;
    • 正则:依赖元字符组合,语法围绕 “字符模式” 展开(如\w匹配字母数字,*表示零或多次重复)。

这俩的共同点也就在于均用于简化代码了,但解决的问题域却完全不同。

特性 Lambda 表达式 正则表达式(Regex)
本质 Java 语言特性(函数式编程) 字符串匹配工具(文本模式)
主要用途 简化匿名内部类,实现函数式接口 文本搜索、替换、验证(如邮箱、手机号格式)
语法 (参数) -> { 代码块 } 由普通字符和特殊元字符(如\d*+)组成的模式字符串
应用场景 集合操作、多线程、事件回调等 字符串处理、数据清洗、表单验证等

虽然二者用途不同,但可以配合使用,例如:

1
2
3
4
5
6
7
8
List<String> emails = Arrays.asList(
"user@example.com", "invalid-email", "admin@test.org"
);

// 使用Lambda + Regex过滤有效邮箱
List<String> validEmails = emails.stream()
.filter(email -> email.matches("^[\\w.-]+@[\\w.-]+\\.[a-z]{2,6}$"))
.toList();
  • 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
2
3
4
5
6
7
() -> System.out.println("Hello World");

// 应用场景示例:实现Runnable接口
Runnable task = () -> {
System.out.println("Task executed");
};
new Thread(task).start();

单个参数(可省略括号):

1
2
3
4
5
6
7
8
x -> x * 2;  // 等价于 (x) -> x * 2;

// 应用场景示例:集合遍历
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(x -> System.out.println(x));

// 类型显式声明示例
Predicate<String> isLong = (String s) -> s.length() > 10;

多个参数:

1
2
3
4
5
(x, y) -> x + y;

// 应用场景示例:Comparator接口
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.sort((a, b) -> a.length() - b.length());

复杂方法体(需要大括号和return语句):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(x, y) -> {
int sum = x + y;
int product = x * y;
return sum + product;
};

// 应用场景示例:自定义处理逻辑
Function<String, Integer> stringProcessor = (str) -> {
if (str == null || str.isEmpty()) {
return 0;
}
return str.chars().filter(c -> Character.isLetter(c)).count();
};

// 检查型异常需要在方法体中显式处理
Supplier<File> fileReader = () -> {
try {
return new File("data.txt");
} catch (IOException e) {
throw new RuntimeException(e);
}
};

无返回值有形参的抽象方法:

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
public class MyTest1 {
public static void main(String[] args) {
MyInterface myInterface = new MyInterface() {
@Override
public void show(int a, int b) {
System.out.println(a + b);
}
};

myInterface.show(20, 30);//50

//简写1:方法名可以自己推断出来
MyInterface myInterface1 = (int a, int b) -> {
System.out.println(a + b);
};

myInterface1.show(20, 40);//60

//简写2:可以省略形参列表中的形参类型
MyInterface myInterface2 = (a, b) -> {
System.out.println(a + b);//70
};

myInterface2.show(20, 50);

//简写3:如果抽象方法中只有一行代码,可以省略方法体的大括号,当然,如果不止一行,就不能省略
MyInterface myInterface3 = (a, b) -> System.out.println(a + b);
myInterface3.show(20, 60);//80
}
}
  • 可以省略方法名,IDEA会帮你自动检测方法名;
  • 可以省略方法中的形参类型;
  • 如果对抽象方法的实现逻辑只有一行,可以省略方法体的大括号,当然如果不止一行,就不能省略了;

有返回值的抽象方法:

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
public class MyTest2 {
public static void main(String[] args) {
MyInterface1 test1 = new MyInterface1() {
@Override
public int test(int a, int b) {
return a - b;
}
};
System.out.println(test1.test(90, 8));//82

//简写1:
MyInterface1 test2 = (int a, int b) -> {
return a - b;
};
System.out.println(test2.test(20, 10));//10

//简写2:
MyInterface1 test3 = (a, b) -> {return a - b;};
System.out.println(test3.test(30, 10));//20

//简写3:这个有返回值的方法,不能直接去掉大括号,还需要去掉return关键字
MyInterface1 test4 = (a, b) -> a - b;
System.out.println(test4.test(40, 10));//30
}
}

  • 有返回值的方法,如果要去掉大括号,还需要去掉return关键字;
  • 形参列表中只有一个参数,可以去掉形参的括号;

基本用法

假设我们有一个函数接口 Calculator

1
2
3
4
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}

我们可以使用 Lambda 表达式为这个接口定义行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class LambdaBasic {
public static void main(String[] args) {
// 1. 完整写法:指定参数类型,方法体用 {} 包裹
Calculator addition1 = (int a, int b) -> { return a + b; };

// 2. 简化1:省略参数类型(编译器自动推断为 int)
Calculator addition2 = (a, b) -> { return a + b; };

// 3. 简化2:方法体只有一行代码时,省略 {} 和 return
Calculator addition3 = (a, b) -> a + b;

// 4. 减法示例:同样支持简化
Calculator subtraction = (a, b) -> a - b;

// 调用测试
System.out.println("10 + 5 = " + addition3.calculate(10, 5)); // 输出:15
System.out.println("10 - 5 = " + subtraction.calculate(10, 5)); // 输出:5
}
}

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
// 函数式接口:接收 String,返回 int
@FunctionalInterface
interface StringProcessor {
int process(String str);
}

public class LambdaBinding {
public static void main(String[] args) {
// 实现:返回字符串长度
StringProcessor lengthProcessor = str -> str.length();
System.out.println(lengthProcessor.process("Lambda")); // 输出:6

// 实现:返回字符串中大写字母的数量
StringProcessor upperCountProcessor = str -> {
int count = 0;
for (char c : str.toCharArray()) {
if (Character.isUpperCase(c)) {
count++;
}
}
return count;
};
System.out.println(upperCountProcessor.process("Hello Lambda")); // 输出:2(H 和 L)
}
}

Lambda 表达式可以访问外部变量,但有严格的限制:

  • 访问局部变量:局部变量必须是最终的(final)或事实上的最终变量(即赋值后不再修改)。

    1
    2
    3
    4
    5
    6
    int 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
    18
    public 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
2
3
4
5
6
7
8
9
import java.util.Arrays;
public class MyTest4 {
public static void main(String[] args) {
Integer[] ints = {89, 67, 23};
Arrays.sort(ints, (o1, o2) -> o1-o2);
System.out.println(Arrays.toString(ints));
//[23, 67, 89]
}
}

而且Lambda 表达式在集合操作、多线程等场景中应用广泛,例如:

  • 集合遍历(替代匿名内部类)
1
2
3
4
5
6
7
8
9
10
11
12
List<String> fruits = Arrays.asList("Apple", "Banana", "Orange");

// 传统方式:使用匿名内部类
fruits.forEach(new Consumer<String>() {
@Override
public void accept(String fruit) {
System.out.println(fruit);
}
});

// Lambda 简化
fruits.forEach(fruit -> System.out.println(fruit));
  • 线程创建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 传统方式:匿名 Runnable
    Thread thread1 = new Thread(new Runnable() {
    @Override
    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,不计入)
    @FunctionalInterface
    interface MyInterface {
    void doSomething(); // 唯一抽象方法
    String toString(); // 继承自Object,不算新增抽象方法
    }
  • 可通过@FunctionalInterface注解显式标记,编译器会自动验证接口是否符合函数式接口规范(若不符合则报错),自定义函数式接口也需要如上注解

    1
    2
    3
    4
    5
    6
    // 错误示例:包含2个抽象方法,编译报错
    @FunctionalInterface
    interface InvalidInterface {
    void method1();
    void method2(); // 编译器报错:函数式接口只能有一个抽象方法
    }
  • 此外,函数式接口可以继承其他接口,但需保证最终抽象方法数量仍为 1:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 父接口(函数式接口)
    @FunctionalInterface
    interface Parent {
    void parentMethod();
    }

    // 子接口:继承父接口,且不新增抽象方法(仍为函数式接口)
    @FunctionalInterface
    interface Child extends Parent {
    default void childMethod() { ... } // 默认方法,不影响抽象方法数量
    }

Lambda 表达式的本质是函数式接口抽象方法的匿名实现,二者必须满足 “方法签名兼容”:

  • Lambda 的参数列表类型、数量、顺序需与接口抽象方法一致;
  • Lambda 的返回值类型需与接口抽象方法兼容(若接口方法有返回值,Lambda 的返回值需可赋值给该类型)。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 函数式接口
@FunctionalInterface
interface Converter<F, T> {
T convert(F from);
}

// Lambda与接口方法签名兼容
Converter<String, Integer> strToInt = s -> Integer.parseInt(s);
Converter<Integer, String> intToStr = i -> "Number: " + i;

// 错误示例:参数数量不匹配
Converter<String, Integer> error = (s1, s2) -> Integer.parseInt(s1);
// 编译报错:Lambda表达式的参数数量与接口方法不匹配

为避免重复定义通用的函数式接口,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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 非函数式接口(2个抽象方法)
interface MultiMethodInterface {
void method1();
void method2();
}

// 错误:Lambda无法实现多抽象方法接口
MultiMethodInterface obj = () -> System.out.println("test");
// 编译报错:目标类型 MultiMethodInterface 不是函数式接口

// 正确方式:使用匿名内部类
MultiMethodInterface obj = new MultiMethodInterface() {
@Override
public void method1() { ... }
@Override
public void method2() { ... }
};

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
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.function.BiFunction;

public class StaticMethodRef {
public static void main(String[] args) {
// Lambda 表达式:调用 Math 类的静态方法 max
BiFunction<Integer, Integer, Integer> maxLambda = (a, b) -> Math.max(a, b);

// 方法引用:简化为 Math::max
BiFunction<Integer, Integer, Integer> maxRef = Math::max;

System.out.println(maxRef.apply(10, 20)); // 输出:20
}
}

实例方法引用(对象名::实例方法名)

当 Lambda 表达式调用的是某个对象的实例方法时,可使用实例方法引用(需先创建对象)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.function.Supplier;

public class InstanceObjMethodRef {
public static void main(String[] args) {
String str = "Hello, Method Reference";

// Lambda 表达式:调用 str 对象的 length() 方法
Supplier<Integer> lengthLambda = () -> str.length();

// 方法引用:简化为 str::length
Supplier<Integer> lengthRef = str::length;

System.out.println(lengthRef.get()); // 输出:21(字符串长度)
}
}

实例方法引用(类名::实例方法名)

当 Lambda 表达式的第一个参数是方法的调用者,后续参数是方法的参数时,可使用类名引用实例方法。

此时方法的调用者是 Lambda 表达式的第一个参数,因此可以省略参数名,直接通过类名引用方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.function.BiFunction;

public class InstanceClassMethodRef {
public static void main(String[] args) {
// Lambda 表达式:调用 s1 的 compareTo 方法,参数为 s2
BiFunction<String, String, Integer> compareLambda = (s1, s2) -> s1.compareTo(s2);

// 方法引用:简化为 String::compareTo
BiFunction<String, String, Integer> compareRef = String::compareTo;

System.out.println(compareRef.apply("apple", "banana")); // 输出:-1("apple" 在 "banana" 之前)
}
}

构造方法引用(类名::new)

当 Lambda 表达式的逻辑是创建一个对象(即调用构造方法)时,可使用构造方法引用

构造方法引用会根据函数式接口的抽象方法签名,自动匹配对应的构造方法(如无参、单参、多参)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.function.Supplier;
import java.util.function.Function;
import java.util.List;
import java.util.ArrayList;

public class ConstructorRef {
public static void main(String[] args) {
// 1. 无参构造方法引用
// Lambda 表达式:创建 ArrayList 对象
Supplier<List<String>> listSupplierLambda = () -> new ArrayList<>();
// 方法引用:简化为 ArrayList::new
Supplier<List<String>> listSupplierRef = ArrayList::new;
List<String> list = listSupplierRef.get();

// 2. 有参构造方法引用
// Lambda 表达式:调用 String 的有参构造方法(String(char[] value))
Function<char[], String> stringFunctionLambda = chars -> new String(chars);
// 方法引用:简化为 String::new
Function<char[], String> stringFunctionRef = String::new;
String str = stringFunctionRef.apply(new char[]{'h', 'i'});
System.out.println(str); // 输出:hi
}
}

方法引用并非在所有场景下都能替代 Lambda 表达式,它需要满足以下条件:

  1. 方法参数与返回值匹配:被引用的方法的参数列表和返回值类型,必须与函数式接口中抽象方法的参数列表和返回值类型完全一致。 例如,Math.max(int a, int b) 的参数是两个 int,返回 int,因此可匹配 BiFunction<Integer, Integer, Integer> 接口(参数为两个 Integer,返回 Integer)。

  2. Lambda 逻辑仅为方法调用:若 Lambda 表达式中除了调用方法还有其他逻辑(如条件判断、循环),则不能使用方法引用。

    1
    2
    3
    4
    5
    // 错误:Lambda 包含额外逻辑,无法用方法引用替代
    Function<Integer, Integer> invalidRef = num -> {
    System.out.println("处理中..."); // 额外逻辑
    return Math.abs(num);
    };