跳到主要内容

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. 参数列表:括号内的参数,可以省略类型
  2. 箭头符号->,将参数和主体分开
  3. 主体:表达式或代码块

语法变体

// 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) -> expressionx -> 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 表达式的三个步骤

  1. 确定类型:找出 Lambda 表达式需要实现的函数式接口类型
  2. 找到方法:确定该接口中唯一的抽象方法
  3. 实现方法:用 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 包中定义了一系列标准函数式接口,覆盖了大多数使用场景。

核心函数式接口

接口抽象方法说明示例用途
Runnablevoid 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 表达式出现的上下文推断目标类型:

  1. 变量声明Predicate<String> p = s -> s.isEmpty();
  2. 赋值p = s -> s.isEmpty();
  3. 返回语句return s -> s.isEmpty();
  4. 方法参数stream.filter(s -> s.isEmpty())
  5. 数组初始化new Predicate[]{s -> s.isEmpty()}
  6. 三元表达式condition ? s -> true : s -> false
  7. 强制类型转换(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::staticMethodNameMath::abs
实例方法引用(特定对象)instance::instanceMethodNameSystem.out::println
实例方法引用(任意对象)ClassName::instanceMethodNameString::toUpperCase
构造函数引用ClassName::newArrayList::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 表达式,原因如下:

  1. 序列化 Lambda 的结果依赖于编译器实现
  2. 不同 JVM 之间的兼容性无法保证
  3. 序列化格式可能在未来版本中改变
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);

小结

本章我们学习了:

  1. Lambda 表达式基础:语法、简化规则、本质
  2. 函数式接口:定义、标准接口、自定义接口
  3. 目标类型:编译器如何推断 Lambda 类型
  4. 方法引用:四种类型及其使用场景
  5. 变量作用域:访问局部变量、成员变量、this 关键字
  6. 实用示例:集合操作、文件处理、事件系统
  7. 最佳实践:保持简洁、优先方法引用、注意性能

Lambda 表达式让 Java 具备了函数式编程能力,配合 Stream API,可以写出更加简洁、高效、可读的代码。

练习

  1. 将以下匿名类转换为 Lambda 表达式:

    new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
    return a.length() - b.length();
    }
    };
  2. 使用 Lambda 和 Stream API 找出列表中所有的偶数,并计算它们的平方和

  3. 编写一个方法,接受一个字符串列表和一个 Predicate,返回满足条件的字符串数量

  4. 使用方法引用重写以下代码:

    list.stream()
    .map(s -> s.toLowerCase())
    .forEach(s -> System.out.println(s));
  5. 解释以下代码为什么编译错误,并给出修复方案:

    int count = 0;
    list.forEach(item -> count++);

参考资料