Java Lambda 表达式
Lambda 表达式是 Java 8 引入的重要特性,它允许我们将函数作为参数传递,使代码更加简洁和灵活。Lambda 表达式的核心思想是"将行为参数化"——把代码块当作数据传递,而不是仅仅传递数据本身。
什么是 Lambda 表达式?
Lambda 表达式是一种匿名函数,它可以没有名称,可以作为参数传递或作为返回值。简单来说,Lambda 表达式让你可以用更简洁的方式表达只包含一个方法的类的实例。
为什么需要 Lambda 表达式?
在 Java 8 之前,如果你想要将一段代码传递给某个方法,你需要创建一个类来实现相应的接口。比如,要定义一个按钮的点击事件处理:
// Java 8 之前:使用匿名类
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("按钮被点击");
}
});
这种方式虽然可行,但对于只包含一个方法的接口来说,语法显得冗长且不必要。Lambda 表达式简化了这种代码:
// Java 8:使用 Lambda 表达式
button.addActionListener(e -> System.out.println("按钮被点击"));
Lambda 表达式的本质
Lambda 表达式本质上是函数式接口的实现。函数式接口是只包含一个抽象方法的接口。当你编写一个 Lambda 表达式时,编译器会自动推断出它实现了哪个函数式接口的唯一抽象方法。
Lambda 表达式语法
基本语法
(parameters) -> expression
// 或
(parameters) -> { statements; }
Lambda 表达式由三部分组成:
- 参数列表:括号内的参数,可以省略类型
- 箭头符号:
->,将参数和主体分开 - 主体:表达式或代码块
语法变体
// 1. 无参数
Runnable r1 = () -> System.out.println("Hello");
Runnable r2 = () -> {
System.out.println("Hello");
System.out.println("World");
};
// 2. 单参数(可以省略括号)
Consumer<String> c1 = (s) -> System.out.println(s);
Consumer<String> c2 = s -> System.out.println(s); // 推荐写法
// 3. 多参数
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
// 4. 显式声明参数类型
BiFunction<Integer, Integer, Integer> addExplicit =
(Integer a, Integer b) -> a + b;
// 5. 多行语句需要大括号和 return
Function<Integer, Integer> square = x -> {
int result = x * x;
return result;
};
// 6. 返回布尔值的表达式
Predicate<Integer> isEven = n -> n % 2 == 0;
语法简化规则
| 情况 | 完整语法 | 简化语法 |
|---|---|---|
| 无参数 | () -> expression | 无变化 |
| 单参数 | (x) -> expression | x -> expression |
| 单参数显式类型 | (int x) -> expression | (x) -> expression |
| 单行表达式 | x -> { return x * 2; } | x -> x * 2 |
最佳实践:保持 Lambda 表达式简洁,最好是一行代码。如果逻辑复杂,考虑提取到单独的方法,然后使用方法引用。
函数式接口
什么是函数式接口?
函数式接口(Functional Interface)是只包含一个抽象方法的接口。Java 8 开始,接口可以包含默认方法和静态方法,这些方法不计入抽象方法数量。
@FunctionalInterface
public interface MyFunctionalInterface {
// 唯一的抽象方法
void doSomething();
// 默认方法(不计入)
default void defaultMethod() {
System.out.println("默认方法");
}
// 静态方法(不计入)
static void staticMethod() {
System.out.println("静态方法");
}
}
@FunctionalInterface 注解是可选的,但推荐使用。它让编译器检查接口是否确实只有一个抽象方法,如果不是则编译失败。
编写 Lambda 表达式的三个步骤
- 确定类型:找出 Lambda 表达式需要实现的函数式接口类型
- 找到方法:确定该接口中唯一的抽象方法
- 实现方法:用 Lambda 语法实现这个方法
以 Predicate<String> 为例:
// 步骤1:确定类型是 Predicate<String>
// 步骤2:找到抽象方法是 boolean test(String t)
// 步骤3:实现这个方法
Predicate<String> isLong = (String s) -> {
return s.length() > 5;
};
// 简化写法
Predicate<String> isLong = s -> s.length() > 5;
// 使用
boolean result = isLong.test("HelloWorld"); // true
java.util.function 包中的函数式接口
Java 8 在 java.util.function 包中定义了一系列标准函数式接口,覆盖了大多数使用场景。
核心函数式接口
| 接口 | 抽象方法 | 说明 | 示例用途 |
|---|---|---|---|
Runnable | void run() | 无参数无返回值 | 执行动作 |
Supplier<T> | T get() | 无参数有返回值 | 懒加载、工厂 |
Consumer<T> | void accept(T t) | 有参数无返回值 | 消费数据、打印 |
Function<T,R> | R apply(T t) | 有参数有返回值 | 数据转换 |
Predicate<T> | boolean test(T t) | 返回布尔值 | 条件判断、过滤 |
基本类型的特化接口
为了避免自动装箱的性能开销,Java 提供了基本类型的特化版本:
| 接口 | 说明 |
|---|---|
IntSupplier, LongSupplier, DoubleSupplier | 返回基本类型 |
IntConsumer, LongConsumer, DoubleConsumer | 消费基本类型 |
IntFunction<R>, LongFunction<R>, DoubleFunction<R> | 基本类型转引用类型 |
IntPredicate, LongPredicate, DoublePredicate | 基本类型判断 |
ToIntFunction<T>, ToLongFunction<T>, ToDoubleFunction<T> | 引用类型转基本类型 |
// 使用基本类型特化接口,避免装箱
IntSupplier intSupplier = () -> 42;
int value = intSupplier.getAsInt(); // 返回 int,不是 Integer
ToIntFunction<String> lengthFunc = String::length;
int len = lengthFunc.applyAsInt("Hello"); // 5
双参数版本
| 接口 | 抽象方法 | 说明 |
|---|---|---|
BiConsumer<T,U> | void accept(T t, U u) | 两个参数无返回值 |
BiFunction<T,U,R> | R apply(T t, U u) | 两个参数有返回值 |
BiPredicate<T,U> | boolean test(T t, U u) | 两个参数返回布尔值 |
BinaryOperator<T> | T apply(T t1, T t2) | 两个同类型参数,返回同类型 |
UnaryOperator<T> | T apply(T t) | 单参数,返回同类型 |
// BiFunction 示例
BiFunction<String, String, String> concat = (a, b) -> a + b;
String result = concat.apply("Hello, ", "World"); // "Hello, World"
// BinaryOperator 示例(两个操作数和结果都是同一类型)
BinaryOperator<Integer> sum = (a, b) -> a + b;
Integer total = sum.apply(10, 20); // 30
// UnaryOperator 示例(输入输出同类型)
UnaryOperator<String> upper = String::toUpperCase;
String upperStr = upper.apply("hello"); // "HELLO"
自定义函数式接口
当标准接口不满足需求时,可以自定义:
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v);
}
// 使用
TriFunction<Integer, Integer, Integer, Integer> sumThree = (a, b, c) -> a + b + c;
int result = sumThree.apply(1, 2, 3); // 6
目标类型与类型推断
什么是目标类型?
Lambda 表达式本身没有类型,它的类型由上下文决定,称为"目标类型"(Target Type)。同一个 Lambda 表达式在不同上下文中可以有不同的类型:
// 同样的 Lambda 表达式 () -> "done" 有不同的目标类型
// 目标类型:Callable<String>
Callable<String> callable = () -> "done";
// 目标类型:Supplier<String>
Supplier<String> supplier = () -> "done";
// 目标类型:自定义函数式接口
interface MySupplier {
String get();
}
MySupplier mySupplier = () -> "done";
编译器如何推断类型
编译器根据 Lambda 表达式出现的上下文推断目标类型:
- 变量声明:
Predicate<String> p = s -> s.isEmpty(); - 赋值:
p = s -> s.isEmpty(); - 返回语句:
return s -> s.isEmpty(); - 方法参数:
stream.filter(s -> s.isEmpty()) - 数组初始化:
new Predicate[]{s -> s.isEmpty()} - 三元表达式:
condition ? s -> true : s -> false - 强制类型转换:
(Predicate<String>) s -> s.isEmpty()
方法重载与 Lambda
当方法重载时,编译器会根据 Lambda 表达式的目标类型选择合适的方法:
public class OverloadExample {
void execute(Runnable runnable) {
runnable.run();
}
<T> T execute(Callable<T> callable) throws Exception {
return callable.call();
}
public static void main(String[] args) throws Exception {
OverloadExample example = new OverloadExample();
// 编译器选择 execute(Callable<T>)
// 因为 Lambda () -> "done" 返回值,匹配 Callable
String result = example.execute(() -> "done");
// 编译器选择 execute(Runnable)
// 因为 Lambda () -> {} 无返回值,匹配 Runnable
example.execute(() -> System.out.println("Hello"));
}
}
方法引用
方法引用是 Lambda 表达式的简化写法,当 Lambda 表达式只是调用一个现有方法时,可以使用方法引用。
四种方法引用类型
| 类型 | 语法 | 示例 |
|---|---|---|
| 静态方法引用 | ClassName::staticMethodName | Math::abs |
| 实例方法引用(特定对象) | instance::instanceMethodName | System.out::println |
| 实例方法引用(任意对象) | ClassName::instanceMethodName | String::toUpperCase |
| 构造函数引用 | ClassName::new | ArrayList::new |
静态方法引用
// Lambda 形式
Function<String, Integer> parser1 = s -> Integer.parseInt(s);
// 方法引用形式
Function<String, Integer> parser2 = Integer::parseInt;
// 使用
Integer num = parser2.apply("123"); // 123
实例方法引用(特定对象)
// Lambda 形式
Consumer<String> printer1 = s -> System.out.println(s);
// 方法引用形式
Consumer<String> printer2 = System.out::println;
// 使用特定实例
StringBuilder sb = new StringBuilder();
Consumer<String> appender = sb::append;
appender.accept("Hello");
实例方法引用(任意对象)
这种引用看起来像静态方法引用,但实际上引用的是实例方法。第一个参数作为方法的调用者:
// Lambda 形式
Function<String, Integer> length1 = s -> s.length();
// 方法引用形式(注意:String 是类名,length 是实例方法)
Function<String, Integer> length2 = String::length;
// BiFunction 示例:第一个参数是调用者
BiFunction<String, String, Boolean> equals = String::equals;
// 等价于
BiFunction<String, String, Boolean> equalsLambda = (s1, s2) -> s1.equals(s2);
// 使用
boolean result = equals.apply("hello", "hello"); // true
构造函数引用
// 无参构造函数
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> list = listSupplier.get();
// 带参数的构造函数
Function<Integer, List<String>> listWithCapacity = ArrayList::new;
List<String> list2 = listWithCapacity.apply(100); // 初始容量 100
// 数组构造函数引用
Function<Integer, String[]> arrayCreator = String[]::new;
String[] array = arrayCreator.apply(5); // 创建长度为 5 的数组
方法引用示例对比
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// 1. 静态方法引用
names.stream()
.map(s -> s.toUpperCase())
.map(String::toUpperCase); // 等价写法
// 2. 实例方法引用
names.forEach(s -> System.out.println(s));
names.forEach(System.out::println); // 等价写法
// 3. 排序
names.sort((a, b) -> a.compareToIgnoreCase(b));
names.sort(String::compareToIgnoreCase); // 等价写法
// 4. 构造函数引用
names.stream()
.map(s -> new StringBuilder(s))
.map(StringBuilder::new); // 等价写法
变量作用域
访问局部变量
Lambda 表达式可以访问外部的局部变量,但这些变量必须是 final 或 effectively final(事实上不可变):
public void example() {
// effectively final 变量(可以不加 final 关键字)
String prefix = "Hello, ";
// Lambda 可以读取
Consumer<String> greeter = name -> System.out.println(prefix + name);
greeter.accept("Alice"); // Hello, Alice
// 不能修改被捕获的变量
// prefix = "Hi, "; // 编译错误
}
为什么有这个限制?
Lambda 表达式可能在另一个线程中执行,如果允许修改局部变量,会引入并发问题。Java 采用"值捕获"而非"变量捕获",即 Lambda 捕获的是变量的值副本,而非变量本身的引用。
访问成员变量
与局部变量不同,Lambda 可以自由访问和修改类的成员变量和静态变量:
public class Counter {
private int instanceCount = 0;
private static int staticCount = 0;
public void increment() {
Runnable r = () -> {
instanceCount++; // 可以修改实例变量
staticCount++; // 可以修改静态变量
};
r.run();
}
}
this 关键字
Lambda 表达式不会引入新的作用域,this 指向创建 Lambda 的外部类实例:
public class ThisExample {
private String name = "Outer";
public void test() {
// Lambda 中 this 指向 ThisExample 实例
Runnable r = () -> {
System.out.println(this.name); // "Outer"
System.out.println(this.getClass().getSimpleName()); // "ThisExample"
};
// 对比:匿名类中的 this 指向匿名类实例本身
Runnable r2 = new Runnable() {
private String name = "Anonymous";
@Override
public void run() {
System.out.println(this.name); // "Anonymous"
System.out.println(this.getClass().getSimpleName()); // ""
}
};
r.run();
r2.run();
}
}
理解 effectively final
Effectively final 意味着变量在初始化后没有被重新赋值:
public void demo() {
String message = "Hello";
// 正确:message 没有被修改,是 effectively final
Runnable r1 = () -> System.out.println(message);
message = "World"; // 重新赋值
// 错误:message 不再是 effectively final
// Runnable r2 = () -> System.out.println(message); // 编译错误
// 变通方法:使用 final 或 effectively final 的中间变量
final String finalMessage = message;
Runnable r3 = () -> System.out.println(finalMessage); // 正确
}
实用示例
1. 集合操作
import java.util.*;
import java.util.stream.Collectors;
public class CollectionExamples {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");
// 过滤:保留长度大于 3 的名字
List<String> filtered = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
System.out.println(filtered); // [Alice, Charlie, David]
// 转换:将名字转为大写
List<String> uppercased = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
// 排序:按名字长度排序
List<String> sorted = names.stream()
.sorted(Comparator.comparingInt(String::length))
.collect(Collectors.toList());
// 查找:找到第一个以 'A' 开头的名字
Optional<String> firstA = names.stream()
.filter(name -> name.startsWith("A"))
.findFirst();
// 统计:计算名字总长度
int totalLength = names.stream()
.mapToInt(String::length)
.sum();
// 分组:按首字母分组
Map<Character, List<String>> grouped = names.stream()
.collect(Collectors.groupingBy(name -> name.charAt(0)));
// 去重
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3);
List<Integer> distinct = numbers.stream()
.distinct()
.collect(Collectors.toList());
}
}
2. 文件处理
import java.io.*;
import java.nio.file.*;
import java.util.stream.*;
public class FileExamples {
public static void main(String[] args) throws IOException {
// 读取文件行
try (Stream<String> lines = Files.lines(Paths.get("data.txt"))) {
lines.filter(line -> !line.startsWith("#")) // 过滤注释行
.map(String::trim) // 去除空白
.forEach(System.out::println);
}
// 列出目录中的所有 Java 文件
try (Stream<Path> paths = Files.list(Paths.get("."))) {
paths.filter(path -> path.toString().endsWith(".java"))
.forEach(System.out::println);
}
}
}
3. 并行处理
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
public class ParallelExamples {
public static void main(String[] args) {
List<Integer> numbers = IntStream.rangeClosed(1, 100)
.boxed()
.collect(Collectors.toList());
// 并行求和
int sum = numbers.parallelStream()
.mapToInt(Integer::intValue)
.sum();
// 并行处理(适合 CPU 密集型任务)
List<String> processed = numbers.parallelStream()
.map(n -> processNumber(n))
.collect(Collectors.toList());
}
private static String processNumber(int n) {
// 模拟耗时处理
return "Number: " + n;
}
}
4. 构建复杂查询
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
public class PersonQuery {
public static void main(String[] args) {
List<Person> people = Arrays.asList(
new Person("Alice", 25, "Engineering"),
new Person("Bob", 30, "Marketing"),
new Person("Charlie", 35, "Engineering"),
new Person("Diana", 28, "Sales")
);
// 组合多个谓词
Predicate<Person> isEngineer = p -> "Engineering".equals(p.department);
Predicate<Person> isUnder30 = p -> p.age < 30;
Predicate<Person> engineerUnder30 = isEngineer.and(isUnder30);
// 查询:工程部门且年龄小于 30 的员工
List<Person> result = people.stream()
.filter(engineerUnder30)
.sorted(Comparator.comparing(p -> p.name))
.collect(Collectors.toList());
// 提取名字列表
List<String> names = people.stream()
.map(p -> p.name)
.collect(Collectors.toList());
// 创建名字到人的映射
Map<String, Person> nameToPerson = people.stream()
.collect(Collectors.toMap(p -> p.name, p -> p));
}
static class Person {
String name;
int age;
String department;
Person(String name, int age, String department) {
this.name = name;
this.age = age;
this.department = department;
}
}
}
5. 实现简单的事件处理系统
import java.util.*;
import java.util.function.*;
public class EventBus {
private final Map<Class<?>, List<Consumer<?>>> handlers = new HashMap<>();
// 注册事件处理器
public <T> void on(Class<T> eventType, Consumer<T> handler) {
handlers.computeIfAbsent(eventType, k -> new ArrayList<>()).add(handler);
}
// 触发事件
@SuppressWarnings("unchecked")
public <T> void emit(T event) {
List<Consumer<?>> handlers = this.handlers.get(event.getClass());
if (handlers != null) {
handlers.forEach(handler -> ((Consumer<T>) handler).accept(event));
}
}
public static void main(String[] args) {
EventBus bus = new EventBus();
// 使用 Lambda 注册处理器
bus.on(String.class, message -> System.out.println("收到消息: " + message));
bus.on(Integer.class, number -> System.out.println("收到数字: " + number));
// 触发事件
bus.emit("Hello World");
bus.emit(42);
}
}
Lambda 与匿名类的区别
| 特性 | 匿名类 | Lambda 表达式 |
|---|---|---|
| 语法 | 冗长,需要 new 关键字和类体 | 简洁,只需要参数和方法体 |
| this 关键字 | 指向匿名类实例本身 | 指向外部类实例 |
| 作用域 | 创建新的作用域 | 不创建新作用域 |
| 编译方式 | 编译为独立的 class 文件 | 使用 invokedynamic 指令 |
| 性能 | 每次创建新实例 | 可能复用实例 |
| 可包含状态 | 可以有成员变量 | 不能有成员变量 |
| 实现多个方法 | 可以实现多个方法(抽象类) | 只能实现单个抽象方法 |
public class Comparison {
private String name = "Outer";
public void test() {
// 匿名类
Runnable anonymous = new Runnable() {
private String name = "Anonymous";
@Override
public void run() {
System.out.println("匿名类 this.name: " + this.name);
System.out.println("匿名类 Outer.this.name: " + Comparison.this.name);
}
};
// Lambda 表达式
Runnable lambda = () -> {
// 没有 Lambda 自己的 this
System.out.println("Lambda this.name: " + this.name);
};
anonymous.run();
// 输出:
// 匿名类 this.name: Anonymous
// 匿名类 Outer.this.name: Outer
lambda.run();
// 输出:
// Lambda this.name: Outer
}
}
序列化
Lambda 表达式可以序列化,前提是目标类型和捕获的参数都可序列化。但是,强烈不推荐序列化 Lambda 表达式,原因如下:
- 序列化 Lambda 的结果依赖于编译器实现
- 不同 JVM 之间的兼容性无法保证
- 序列化格式可能在未来版本中改变
import java.io.*;
// 不推荐的做法
public class LambdaSerialization {
public static void main(String[] args) throws Exception {
// 目标类型可序列化
Runnable r = (Runnable & Serializable) () -> System.out.println("Hello");
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(r);
// 反序列化
ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(bos.toByteArray()));
Runnable restored = (Runnable) ois.readObject();
restored.run();
}
}
替代方案:如果需要序列化,使用传统的命名类或匿名类。
最佳实践
1. 保持 Lambda 表达式简洁
// 不好:Lambda 太长
list.stream()
.filter(s -> {
if (s == null) return false;
String trimmed = s.trim();
return trimmed.length() > 0 && trimmed.charAt(0) == 'A';
})
.collect(Collectors.toList());
// 好:提取到方法
list.stream()
.filter(this::isValidString)
.collect(Collectors.toList());
private boolean isValidString(String s) {
if (s == null) return false;
String trimmed = s.trim();
return trimmed.length() > 0 && trimmed.charAt(0) == 'A';
}
2. 优先使用方法引用
// 不够简洁
list.stream()
.map(s -> s.toUpperCase())
.filter(s -> s.length() > 3)
.forEach(s -> System.out.println(s));
// 更好:使用方法引用
list.stream()
.map(String::toUpperCase)
.filter(s -> s.length() > 3)
.forEach(System.out::println);
3. 使用标准函数式接口
// 不好:定义新的函数式接口
@FunctionalInterface
interface StringProcessor {
String process(String s);
}
// 好:使用标准接口
Function<String, String> processor = s -> s.toUpperCase();
4. 谨慎使用 Lambda 访问外部变量
// 不好:尝试修改外部变量
int sum = 0;
list.forEach(item -> {
// sum += item; // 编译错误
});
// 好:使用 Stream 的 reduce
int sum = list.stream().mapToInt(Integer::intValue).sum();
// 或者使用可变容器
int[] sumHolder = {0};
list.forEach(item -> sumHolder[0] += item);
5. 注意装箱和拆箱开销
// 有装箱开销
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b); // Integer 自动拆箱
// 更好:使用基本类型流
int sum = numbers.stream()
.mapToInt(Integer::intValue)
.sum(); // 无装箱开销
6. Lambda 表达式不应该抛出受检异常
// 问题:Lambda 抛出受检异常
list.forEach(item -> {
// throws IOException
Files.write(path, item.getBytes()); // 编译错误
});
// 解决方案 1:包装为非受检异常
list.forEach(item -> {
try {
Files.write(path, item.getBytes());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
// 解决方案 2:提取到方法
list.forEach(this::writeItem);
小结
本章我们学习了:
- Lambda 表达式基础:语法、简化规则、本质
- 函数式接口:定义、标准接口、自定义接口
- 目标类型:编译器如何推断 Lambda 类型
- 方法引用:四种类型及其使用场景
- 变量作用域:访问局部变量、成员变量、this 关键字
- 实用示例:集合操作、文件处理、事件系统
- 最佳实践:保持简洁、优先方法引用、注意性能
Lambda 表达式让 Java 具备了函数式编程能力,配合 Stream API,可以写出更加简洁、高效、可读的代码。
练习
-
将以下匿名类转换为 Lambda 表达式:
new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
}; -
使用 Lambda 和 Stream API 找出列表中所有的偶数,并计算它们的平方和
-
编写一个方法,接受一个字符串列表和一个 Predicate,返回满足条件的字符串数量
-
使用方法引用重写以下代码:
list.stream()
.map(s -> s.toLowerCase())
.forEach(s -> System.out.println(s)); -
解释以下代码为什么编译错误,并给出修复方案:
int count = 0;
list.forEach(item -> count++);