摘要: 本文深入讲解Java 8引入的Lambda表达式,从匿名内部类的痛点出发,详细解析Lambda的基本语法、函数式接口、方法引用等核心概念。通过丰富的代码示例展示Lambda如何简化集合操作、实现变量捕获,并与var关键字配合使用。最后通过学生管理系统重构案例,帮助读者掌握Lambda在实际开发中的应用,为后续学习Stream API打下坚实基础。
关键词: Lambda表达式, 函数式接口, 方法引用, Java 8, 集合操作
在上一篇我们学习了全新的日期时间 API,你感受到了现代 Java 在 API 设计上的优雅和强大。今天我们要聊的,是另一个彻底改变 Java 面貌的特性——Lambda 表达式。
回忆一下,我们在学习集合框架时,写过这样的代码:
// 对一个 List 进行排序
List<String> names = Arrays.asList("张三", "李四", "王五");
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
这段代码其实很简单:对名字列表按字典序排序。但它写了整整 6 行——真正核心的逻辑只有一行 return a.compareTo(b);,其余全是“样板代码”:new Comparator<String>()、@Override、public int compare(...)。
这种感觉就像你想点一杯咖啡,但必须先填三张表格、盖两个章、再写一份申请书。明明只是一件简单的事,却被一层又一层的“仪式感”包裹着。
Java 的设计者们在 2014 年(Java 8)终于解决了这个问题,引入了 Lambda 表达式。它让你把“行为”当作数据一样传递,用一种极其简洁的语法来表达“我要做什么”,而不是“我要怎么用代码实现它”。
上面的代码,用 Lambda 可以简化为一行:
Collections.sort(names, (a, b) -> a.compareTo(b));
甚至更简洁(使用方法引用):
Collections.sort(names, String::compareTo);
是不是清爽了很多?今天,我们就来全面学习 Lambda 表达式——它是什么、怎么写、用在哪儿,以及它如何开启了 Java 函数式编程的大门。
1. 从“匿名内部类”到“Lambda”—— 一个真实的需求
1.1 匿名内部类的痛点
在 Java 8 之前,如果你想传递一个“行为”(比如“比较两个字符串”),你只能用匿名内部类:
// 场景一:线程
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程执行中...");
}
});
// 场景二:排序
List<Student> students = ...;
students.sort(new Comparator<Student>() {
@Override
public int compare(Student s1, Student s2) {
return s1.getAge() - s2.getAge();
}
});
// 场景三:事件监听(GUI 或 Android)
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
System.out.println("按钮被点击了!");
}
});
你看,每次我们只是想“做一件简单的事”,却要创建一整块匿名类的“壳”。这个壳里真正有意义的代码(业务逻辑)只占很小一部分。这种写法有三个问题:
- 代码臃肿:大量重复的、没有信息量的模板代码。
- 阅读负担:你想看的逻辑被淹没在语法里。
- 心智负担:你需要理解“匿名内部类”这个相对复杂的概念,才能写出这段简单的功能。
1.2 Lambda 的解决方案
Lambda 表达式的设计目标是:用最少的语法,表达一个可以被执行的代码块。
上面的例子用 Lambda 重写:
// 线程
Thread t = new Thread(() -> System.out.println("线程执行中..."));
// 排序
students.sort((s1, s2) -> s1.getAge() - s2.getAge());
// 事件监听(JavaFX 风格)
button.setOnAction(event -> System.out.println("按钮被点击了!"));
你只需要关心做什么(System.out.println(...)),而不用关心怎么包装成对象。编译器会自动处理所有“包装”的工作。
2. Lambda 的基本语法
Lambda 表达式由三部分组成:
(参数列表) -> { 方法体 }
- 参数列表:和普通方法的参数一样,类型可以省略(编译器自动推断)。
- 箭头
->:固定语法,分隔参数和方法体。 - 方法体:要执行的代码,如果只有一行,可以省略花括号和
return。
2.1 最简形式(单参数、单语句)
// 传统写法
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
// Lambda 写法(无参数,单语句)
Runnable r2 = () -> System.out.println("Hello");
2.2 带参数的 Lambda
// 一个参数,类型可省略
Consumer<String> c = s -> System.out.println(s);
// 两个参数,类型可省略
Comparator<Integer> comp = (a, b) -> a - b;
// 显式指定类型(一般不需要,但有时用于消除歧义)
Comparator<Integer> comp2 = (Integer a, Integer b) -> a - b;
2.3 多语句的方法体
如果方法体包含多条语句,必须用花括号 {} 括起来,并且如果方法有返回值,必须显式写 return:
// 单语句,可以省略花括号和 return
Function<String, Integer> f1 = s -> s.length();
// 多语句,必须用花括号,必须写 return
Function<String, Integer> f2 = s -> {
int len = s.length();
System.out.println("计算长度:" + len);
return len;
};
2.4 参数类型推断
Java 编译器会自动推断参数类型,绝大多数情况下你不需要写类型。但如果你觉得阅读有困难,也可以显式写上:
// 类型推断
Comparator<String> c1 = (a, b) -> a.compareTo(b);
// 显式类型(可选)
Comparator<String> c2 = (String a, String b) -> a.compareTo(b);
2.5 单参数可以省略括号
如果只有一个参数,并且你省略了类型,可以省略括号:
Consumer<String> c = s -> System.out.println(s);
// 等价于 (s) -> System.out.println(s)
// 注意:如果参数类型显式写了,括号不能省
3. 函数式接口 —— Lambda 的“载体”
Lambda 表达式不能单独存在,它必须赋值给一个函数式接口(Functional Interface)。
函数式接口就是只有一个抽象方法的接口。Lambda 表达式会被“翻译”成这个唯一方法的实现。
3.1 常见的函数式接口
Java 标准库在 java.util.function 包中提供了大量现成的函数式接口,覆盖了绝大多数常见场景:
| 接口 | 抽象方法 | 用途 |
|---|---|---|
Runnable | void run() | 无参数、无返回值 |
Supplier<T> | T get() | 无参数、返回 T |
Consumer<T> | void accept(T) | 接收一个参数、无返回值 |
Function<T,R> | R apply(T) | 接收 T、返回 R |
Predicate<T> | boolean test(T) | 接收 T、返回 boolean |
UnaryOperator<T> | T apply(T) | 接收 T、返回 T(Function 的特例) |
BinaryOperator<T> | T apply(T,T) | 接收两个 T、返回 T |
Comparator<T> | int compare(T,T) | 比较两个 T |
3.2 使用示例
import java.util.function.*;
public class FunctionalInterfaceDemo {
public static void main(String[] args) {
// Supplier:提供数据
Supplier<String> supplier = () -> "Hello, Lambda!";
System.out.println(supplier.get());
// Consumer:消费数据
Consumer<String> consumer = s -> System.out.println("消费:" + s);
consumer.accept("你好");
// Function:转换数据
Function<String, Integer> function = s -> s.length();
System.out.println("长度:" + function.apply("Java"));
// Predicate:判断条件
Predicate<Integer> predicate = n -> n > 0;
System.out.println("5 是正数?" + predicate.test(5));
System.out.println("-3 是正数?" + predicate.test(-3));
// BinaryOperator:二元运算
BinaryOperator<Integer> add = (a, b) -> a + b;
System.out.println("3 + 5 = " + add.apply(3, 5));
}
}
3.3 自定义函数式接口
你也可以定义自己的函数式接口,用 @FunctionalInterface 注解来标记。这个注解不是必须的,但它能让编译器帮你检查是否真的只有一个抽象方法。
@FunctionalInterface
public interface Calculator {
int calculate(int a, int b);
// 可以有默认方法
default void print() {
System.out.println("计算结果...");
}
// 可以有静态方法
static void info() {
System.out.println("Calculator 接口");
}
// 不能有第二个抽象方法,否则 @FunctionalInterface 会报错
}
// 使用
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;
System.out.println(add.calculate(3, 5)); // 8
System.out.println(multiply.calculate(3, 5)); // 15
4. 方法引用 —— 让 Lambda 更简洁
当你实现的功能仅仅是“调用一个已有的方法”时,Lambda 还可以进一步简化为方法引用(Method Reference)。
方法引用使用 :: 符号,有四种形式:
| 类型 | 语法 | 示例 |
|---|---|---|
| 静态方法引用 | 类名::静态方法名 | Math::max |
| 实例方法引用(特定对象) | 对象::实例方法名 | list::add |
| 实例方法引用(特定类型) | 类名::实例方法名 | String::length |
| 构造方法引用 | 类名::new | ArrayList::new |
4.1 静态方法引用
// Lambda
Function<String, Integer> f1 = s -> Integer.parseInt(s);
// 方法引用
Function<String, Integer> f2 = Integer::parseInt;
// 使用
System.out.println(f2.apply("123")); // 123
4.2 实例方法引用(特定对象)
List<String> list = new ArrayList<>();
// Lambda
Consumer<String> c1 = s -> list.add(s);
// 方法引用
Consumer<String> c2 = list::add;
c2.accept("Hello");
c2.accept("World");
System.out.println(list); // [Hello, World]
4.3 实例方法引用(特定类型)
当 Lambda 的第一个参数是方法的调用者时,可以用这种形式:
// Lambda:s 是调用者,s.length() 是实例方法
Function<String, Integer> f1 = s -> s.length();
// 方法引用:String 类型,实例方法 length
Function<String, Integer> f2 = String::length;
System.out.println(f2.apply("Java")); // 4
// 再举一个例子
// Lambda:两个参数,第一个是调用者,第二个是参数
BiPredicate<String, String> p1 = (s1, s2) -> s1.equals(s2);
// 方法引用:第一个参数类型 String,实例方法 equals
BiPredicate<String, String> p2 = String::equals;
System.out.println(p2.test("A", "A")); // true
4.4 构造方法引用
// Lambda
Supplier<List<String>> s1 = () -> new ArrayList<>();
// 构造方法引用
Supplier<List<String>> s2 = ArrayList::new;
List<String> list = s2.get(); // 创建一个新的 ArrayList
4.5 方法引用在实际中的应用
在集合操作中,方法引用大量使用:
List<String> names = Arrays.asList("张三", "李四", "王五");
// Lambda 写法
names.forEach(name -> System.out.println(name));
// 方法引用写法(更简洁)
names.forEach(System.out::println);
5. Lambda 与集合的配合 —— 操作如流水
Lambda 让集合操作变得前所未有的流畅。来看几个例子:
5.1 forEach() —— 遍历
List<String> list = Arrays.asList("A", "B", "C");
list.forEach(item -> System.out.println(item));
list.forEach(System.out::println); // 方法引用
5.2 removeIf() —— 条件删除
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6));
numbers.removeIf(n -> n % 2 == 0); // 删除所有偶数
System.out.println(numbers); // [1, 3, 5]
5.3 sort() —— 自定义排序
List<Student> students = ...;
students.sort((s1, s2) -> s1.getAge() - s2.getAge()); // 按年龄升序
students.sort(Comparator.comparing(Student::getAge)); // 更优雅的写法
students.sort(Comparator.comparing(Student::getAge).reversed()); // 降序
5.4 replaceAll() —— 批量替换
List<String> words = Arrays.asList("hello", "world", "java");
words.replaceAll(String::toUpperCase);
System.out.println(words); // [HELLO, WORLD, JAVA]
6. 变量捕获 —— Lambda 的“闭包”
Lambda 表达式可以访问外部的变量,这叫做变量捕获(Variable Capture)。它和匿名内部类类似,但规则稍微宽松了一些。
public class CaptureDemo {
public static void main(String[] args) {
String prefix = ">> "; // 有效 final
Consumer<String> printer = s -> System.out.println(prefix + s);
printer.accept("Hello"); // 输出:>> Hello
// 如果这个变量被修改,就不能在 Lambda 中使用了
// prefix = ">> "; // 取消注释会导致编译错误
}
}
规则:
- 被 Lambda 捕获的变量必须是有效 final(effectively final)——即变量在初始化后没有被修改过。
- 和匿名内部类不同,Lambda 可以捕获非
final的变量,只要它没有被修改。
7. Lambda 与 var 的配合(JDK 10+)
在 JDK 10 之后,你可以在 Lambda 的参数中使用 var:
// 使用 var 让参数类型更明确(但仍然是类型推断)
var list = List.of("A", "B", "C");
list.forEach((var s) -> System.out.println(s));
// 这允许在参数上使用注解(比如 @Nullable)
// list.forEach((@Nullable var s) -> System.out.println(s));
8. Lambda 的局限性
虽然 Lambda 很强大,但它不是万能的:
- 只能用于函数式接口:不能直接赋值给普通类或抽象类。
- 不能捕获可变变量:被捕获的变量必须是有效 final。
this的含义不同:在 Lambda 中,this指向的是外部类的实例(和匿名内部类一样),而不是 Lambda 本身。- 调试困难:由于 Lambda 是匿名函数,堆栈跟踪中可能显示为
lambda$main$0,不如普通方法直观。 - 不能用于递归:Lambda 不能直接调用自身(因为没有名字)。
9. 综合示例 —— 用 Lambda 重构“学生管理系统”的排序
我们回到之前的学生管理系统,用 Lambda 来简化排序和过滤操作:
import java.util.*;
import java.util.function.Predicate;
public class StudentManagerWithLambda {
private List<Student> students = new ArrayList<>();
public void addStudent(Student s) {
students.add(s);
}
// 使用 Predicate 过滤学生(可复用)
public List<Student> filter(Predicate<Student> condition) {
List<Student> result = new ArrayList<>();
for (Student s : students) {
if (condition.test(s)) {
result.add(s);
}
}
return result;
}
// 使用 Comparator 排序(可定制)
public void sort(Comparator<Student> comparator) {
students.sort(comparator);
}
public void printAll() {
students.forEach(System.out::println);
}
public static void main(String[] args) {
StudentManagerWithLambda manager = new StudentManagerWithLambda();
manager.addStudent(new Student("张三", 20, Gender.MALE, "S001", GradeLevel.SOPHOMORE));
manager.addStudent(new Student("李四", 22, Gender.MALE, "S002", GradeLevel.JUNIOR));
manager.addStudent(new Student("王五", 18, Gender.FEMALE, "S003", GradeLevel.FRESHMAN));
manager.addStudent(new Student("赵六", 21, Gender.MALE, "S004", GradeLevel.JUNIOR));
manager.addStudent(new Student("孙七", 19, Gender.FEMALE, "S005", GradeLevel.SOPHOMORE));
System.out.println("=== 按年龄降序排序 ===");
manager.sort((s1, s2) -> s2.getAge() - s1.getAge());
manager.printAll();
System.out.println("\n=== 过滤出大二及以上的学生 ===");
List<Student> upperStudents = manager.filter(s -> s.getGrade().ordinal() >= GradeLevel.SOPHOMORE.ordinal());
upperStudents.forEach(System.out::println);
System.out.println("\n=== 过滤出年龄 >= 20 的学生,并打印姓名 ===");
manager.filter(s -> s.getAge() >= 20)
.stream() // 暂时用 Stream(下一篇会详细讲)
.map(Student::getName)
.forEach(System.out::println);
}
}
10. 今天的总结
今天我们系统学习了 Lambda 表达式:
- 什么是 Lambda:一种简洁的语法,让你把“行为”当作数据传递。
- 基本语法:
(参数) -> { 方法体 },参数类型可以省略。 - 函数式接口:只有一个抽象方法的接口,是 Lambda 的载体。
java.util.function包提供了丰富的函数式接口。 - 方法引用:用
::进一步简化 Lambda,适用于“直接调用已有方法”的场景。 - 变量捕获:Lambda 可以访问有效 final 的外部变量。
- 与集合配合:
forEach、removeIf、sort等方法让集合操作更流畅。 - 局限:只能用于函数式接口,不能捕获可变变量等。
Lambda 表达式是 Java 函数式编程的基石。它让代码变得更简洁、更可读,也让集合操作、事件处理、并发编程等场景变得前所未有的流畅。
动手试试
- 把一个
List<String>中的所有字符串按长度排序(从短到长),用 Lambda 写Comparator。 - 用
removeIf删除一个List<Integer>中所有小于 0 的元素,并打印剩下的元素。 - 定义一个函数式接口
Transformer<T>,包含T transform(T input)方法。用 Lambda 实现两个Transformer:一个把字符串转成大写,一个把字符串前后加***。然后写一个方法applyTransformers(String s, List<Transformer<String>> transformers),依次应用所有转换器并输出结果。 - (挑战)你有一个
List<Student>,用 Lambda 和Comparator实现按年级排序(FRESHMAN → SENIOR),年级相同再按年龄升序排序。(提示:Comparator.comparing和thenComparing)
Lambda 表达式的内容就讲到这里。下一篇文章,我们将正式进入 Stream API 的学习——它是在 Lambda 基础上构建的一套强大的数据处理流水线,让你像写 SQL 一样操作集合。
我们下一篇见。😃
📌 获取本系列示例代码请访问 GitCode。
321



被折叠的 条评论
为什么被折叠?



