[Java8] 函数式编程¶
本文涵盖 Java 8 函数式编程三大核心:Lambda 表达式、Stream API、Optional。
1. Lambda 表达式¶
1.1 引入:它解决了什么问题?¶
Java 8 之前,传递"行为"需要写冗长的匿名内部类。Lambda 表达式让代码更简洁,将行为作为参数传递。
// 传统写法:匿名内部类(5行)
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.compareTo(b);
}
});
// Lambda 写法(1行)
Collections.sort(names, (a, b) -> a.compareTo(b));
// 方法引用(更简洁)
Collections.sort(names, String::compareTo);
为什么 Lambda 能替代匿名内部类:Lambda 只能替代函数式接口(只有一个抽象方法的接口)的匿名内部类。
Comparator只有一个compare方法,所以可以用 Lambda 替代。如果接口有多个抽象方法,Lambda 无法替代。
1.2 Lambda 语法¶
| 形式 | 示例 |
|---|---|
| 无参数 | () -> System.out.println("hello") |
| 单参数(可省略括号) | x -> x * 2 |
| 多参数 | (x, y) -> x + y |
| 多行方法体 | (x, y) -> { int sum = x + y; return sum; } |
1.3 四大函数式接口¶
| 接口 | 方法签名 | 用途 | 示例 | 记忆口诀 |
|---|---|---|---|---|
Function<T, R> | R apply(T t) | 转换:输入T,输出R | Function<String, Integer> f = Integer::parseInt | 有进有出 |
Consumer<T> | void accept(T t) | 消费:输入T,无返回 | Consumer<String> c = System.out::println | 有进无出 |
Supplier<T> | T get() | 供给:无输入,输出T | Supplier<List> s = ArrayList::new | 无进有出 |
Predicate<T> | boolean test(T t) | 断言:输入T,返回boolean | Predicate<String> p = String::isEmpty | 有进出布尔 |
// Function:字符串转整数
Function<String, Integer> toInt = Integer::parseInt;
Integer result = toInt.apply("123"); // 123
// Consumer:打印每个元素
Consumer<String> printer = System.out::println;
printer.accept("Hello Lambda"); // Hello Lambda
// Supplier:延迟创建对象(懒加载)
Supplier<List<String>> listFactory = ArrayList::new;
List<String> list = listFactory.get();
// Predicate:过滤空字符串
Predicate<String> notEmpty = s -> !s.isEmpty();
boolean valid = notEmpty.test("Java"); // true
1.4 方法引用四种形式¶
| 类型 | 语法 | 等价 Lambda | 使用场景 |
|---|---|---|---|
| 静态方法引用 | Integer::parseInt | s -> Integer.parseInt(s) | 调用静态方法 |
| 实例方法引用(特定对象) | str::toUpperCase | () -> str.toUpperCase() | 调用特定对象的方法 |
| 实例方法引用(任意对象) | String::toUpperCase | s -> s.toUpperCase() | 调用参数本身的方法 |
| 构造方法引用 | ArrayList::new | () -> new ArrayList<>() | 创建对象 |
1.5 Lambda 的限制:effectively final¶
int count = 0;
// ❌ 编译错误:count 在 Lambda 外被修改
list.forEach(item -> count++); // Variable used in lambda should be effectively final
// 原因:Lambda 可能在不同线程中执行,如果允许修改外部变量会有并发问题
// 捕获语义:Lambda 捕获的是局部变量的“快照”:
// - 基本类型 → 复制值
// - 引用类型 → 复制“引用”(还是指向同一个对象,可以调用其方法修改对象内部状态,但不能再将变量指向另一个对象)
// 所以要求“局部变量引用本身”不可变,而不是对象内部不可变。
// ✅ 正确:使用 AtomicInteger(线程安全的可变容器)
AtomicInteger count = new AtomicInteger(0);
list.forEach(item -> count.incrementAndGet());
1.6 Lambda 常见问题¶
Q:Lambda 表达式能访问外部变量吗?
可以,但外部局部变量必须是 effectively final(事实上不可变)。捕获语义:基本类型捕获值,引用类型捕获引用的副本(还是同一个对象,可以修改对象内部状态)。要求 final 是针对“局部变量引用本身”而非对象内容——目的是避免将“处于高速变化的栈帧局部变量”告诉一个可能在别的线程、别的时间执行的 Lambda。
Q:Lambda 和匿名内部类有什么区别?
- Lambda 只能替代函数式接口(单抽象方法接口)的匿名内部类;2. Lambda 中的
this指向外部类,匿名内部类中的this指向匿名内部类本身;3. Lambda 没有自己的作用域,匿名内部类有独立作用域。
1.7 Lambda 工作中常见坑¶
❌ 坑1:在 Lambda 中修改外部变量(effectively final 问题)¶
// ❌ 编译报错:count 在 Lambda 外被修改
int count = 0;
list.forEach(item -> {
if (item.isValid()) count++; // Variable used in lambda should be effectively final
});
// ❌ 同样报错:即使在 Lambda 外修改也不行
int count = 0;
list.forEach(item -> System.out.println(count)); // 如果后面有 count = 1 就报错
// ✅ 方案1:用 AtomicInteger(线程安全场景)
AtomicInteger count = new AtomicInteger(0);
list.forEach(item -> { if (item.isValid()) count.incrementAndGet(); });
// ✅ 方案2:用 Stream 的 filter + count(更函数式)
long count = list.stream().filter(Item::isValid).count();
❌ 坑2:Lambda 中的异常处理¶
// ❌ 问题:Lambda 内部抛出 Checked Exception,编译报错
// 因为 Function<T,R> 的 apply 方法没有声明 throws
list.stream()
.map(path -> Files.readString(path)) // 编译报错:IOException 未处理
.collect(Collectors.toList());
// ✅ 方案1:在 Lambda 内部 try-catch(代码丑但直接)
list.stream()
.map(path -> {
try {
return Files.readString(path);
} catch (IOException e) {
throw new RuntimeException(e); // 包装为 RuntimeException
}
})
.collect(Collectors.toList());
// ✅ 方案2:抽取工具方法,统一包装
@FunctionalInterface
interface ThrowingFunction<T, R> {
R apply(T t) throws Exception;
}
static <T, R> Function<T, R> wrap(ThrowingFunction<T, R> f) {
return t -> {
try { return f.apply(t); }
catch (Exception e) { throw new RuntimeException(e); }
};
}
list.stream()
.map(wrap(Files::readString)) // 干净
.collect(Collectors.toList());
❌ 坑3:方法引用与 null 的问题¶
// ❌ 危险:list 中有 null 元素时,方法引用会 NPE
List<String> names = Arrays.asList("Alice", null, "Bob");
names.stream()
.map(String::toUpperCase) // null.toUpperCase() → NPE!
.collect(Collectors.toList());
// ✅ 先过滤 null
names.stream()
.filter(Objects::nonNull)
.map(String::toUpperCase)
.collect(Collectors.toList());
❌ 坑4:Lambda 持有外部对象引用导致内存泄漏¶
// ❌ 危险:Lambda 持有 this 引用,如果 Lambda 被长期持有(如注册到事件总线),
// 会导致外部对象无法被 GC 回收
public class OrderService {
private List<Order> orders = new ArrayList<>();
public void register() {
// Lambda 隐式持有 OrderService.this 的引用
eventBus.subscribe(event -> orders.add(event.getOrder()));
// 如果 eventBus 生命周期比 OrderService 长,OrderService 永远不会被 GC
}
}
// ✅ 注意及时取消订阅,或使用弱引用
2.Stream API¶
2.1 引入:它解决了什么问题?¶
Stream API 让集合操作(过滤、转换、聚合)从命令式循环变为声明式链式调用,代码更简洁、可读性更强。
// 传统写法:找出所有长度>3的名字,转大写,排序
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Di");
List<String> result = new ArrayList<>();
for (String name : names) {
if (name.length() > 3) {
result.add(name.toUpperCase());
}
}
Collections.sort(result);
// Stream 写法(链式,一目了然)
List<String> result = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.sorted()
.collect(Collectors.toList());
2.2 Stream 操作分类¶
flowchart LR
A["数据源<br>Collection/Array"] --> B["中间操作<br>惰性求值"]
B --> C["终止操作<br>触发执行"]
subgraph 中间操作["中间操作 — 返回 Stream,不立即执行"]
D[filter 过滤]
E[map 转换]
F[flatMap 扁平化]
G[sorted 排序]
H[distinct 去重]
I[limit/skip 截取]
J[peek 调试]
end
subgraph 终止操作["终止操作 — 返回结果,触发执行"]
K[collect 收集]
L[forEach 遍历]
M[count 计数]
N[findFirst/findAny]
O[anyMatch/allMatch]
P[reduce 聚合]
end 关键原理:惰性求值 中间操作不会立即执行,只有遇到终止操作时才会触发整个流水线的执行。 为什么这样设计:惰性求值允许短路优化——如
findFirst()找到第一个就停止,不需要处理所有元素;limit(10)只处理前 10 个元素,后面的中间操作根本不执行。
2.3 常用操作示例¶
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// filter + collect:过滤偶数
List<Integer> evens = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList()); // [2, 4, 6, 8, 10]
// map:平方
List<Integer> squares = numbers.stream()
.map(n -> n * n)
.collect(Collectors.toList()); // [1, 4, 9, 16, ...]
// reduce:求和
int sum = numbers.stream()
.reduce(0, Integer::sum); // 55
// groupingBy:按奇偶分组
Map<Boolean, List<Integer>> grouped = numbers.stream()
.collect(Collectors.groupingBy(n -> n % 2 == 0));
// {false=[1,3,5,7,9], true=[2,4,6,8,10]}
// flatMap:扁平化嵌套列表
List<List<Integer>> nested = Arrays.asList(
Arrays.asList(1, 2), Arrays.asList(3, 4));
List<Integer> flat = nested.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList()); // [1, 2, 3, 4]
2.4 综合实战¶
需求:从用户列表中找出年龄大于18岁的活跃用户,取其邮箱,去重后按字母排序。
// 函数式写法:Stream + Lambda + Optional 组合
List<String> getActiveUserEmails(List<User> users) {
return users.stream()
.filter(u -> u.getAge() > 18 && u.isActive()) // 过滤条件
.map(User::getEmail) // 提取邮箱
.filter(Objects::nonNull) // 过滤空值
.distinct() // 去重
.sorted() // 排序
.collect(Collectors.toList()); // 收集结果
}
2.5 Stream 工作中常见坑¶
| 坑点 | 问题描述 | 根本原因 | 解决方案 |
|---|---|---|---|
| Stream 只能消费一次 | 对同一个 Stream 调用两次终止操作会抛异常 | Stream 是一次性的流水线,消费后状态变为"已关闭" | 每次从数据源重新创建 Stream |
| 并行流线程安全 | parallelStream() 操作非线程安全集合会出错 | 并行流在 ForkJoinPool 中多线程执行,共享状态会竞争(可能丢失元素或抛 ArrayIndexOutOfBoundsException) | 使用 collect() 而非直接 add() |
| 空指针异常 | map() 返回 null 后续操作 NPE | Stream 不会自动处理 null 值 | 使用 filter(Objects::nonNull) 或 Optional |
| 性能误区 | 小数据量用 Stream 反而更慢 | Stream 有创建流水线的开销,数据量小时开销占比大 | 数据量小时用普通 for 循环 |
坑点代码示例¶
// ❌ 坑1:Stream 只能消费一次
Stream<String> stream = list.stream().filter(s -> s.length() > 3);
long count = stream.count(); // 第一次消费,OK
List<String> result = stream.collect(Collectors.toList()); // 抛 IllegalStateException!
// ✅ 每次重新创建
long count = list.stream().filter(s -> s.length() > 3).count();
List<String> result = list.stream().filter(s -> s.length() > 3).collect(Collectors.toList());
// ❌ 坑 2:parallelStream 操作非线程安全集合
List<Integer> result = new ArrayList<>();
IntStream.range(0, 1000).parallel().forEach(result::add); // 可能丢失数据或抛 ArrayIndexOutOfBoundsException!
// ✅ 用 collect 收集,线程安全
List<Integer> result = IntStream.range(0, 1000).parallel()
.boxed()
.collect(Collectors.toList());
// ❌ 坑3:map 返回 null 导致后续 NPE
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<Integer> lengths = names.stream()
.map(name -> name.equals("Bob") ? null : name.length()) // Bob 返回 null
.collect(Collectors.toList()); // 不报错,但 list 中有 null
// 后续调用 lengths.stream().mapToInt(Integer::intValue) 会 NPE!
// ✅ 过滤 null
List<Integer> lengths = names.stream()
.map(name -> name.equals("Bob") ? null : name.length())
.filter(Objects::nonNull)
.collect(Collectors.toList());
// ❌ 坑4:在 Stream 中修改外部集合(ConcurrentModificationException)
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
list.stream().forEach(s -> {
if (s.equals("b")) list.remove(s); // 抛 ConcurrentModificationException!
});
// ✅ 用 removeIf
list.removeIf(s -> s.equals("b"));
// ❌ 坑5:peek 用于调试可以,但不要用于修改状态(有副作用)
// peek 是中间操作,在某些短路场景下可能不执行
list.stream()
.peek(s -> System.out.println("处理: " + s)) // 调试用,OK
.filter(s -> s.length() > 1)
.findFirst(); // 找到第一个就停止,后面的 peek 不会执行
2.6 Stream 常见问题¶
Q:Stream 的惰性求值是什么意思?
中间操作(filter/map/sorted)不会立即执行,它们只是构建了一个操作流水线。只有当终止操作(collect/forEach/count)被调用时,整个流水线才会被触发执行。好处是可以进行短路优化(如
findFirst()找到第一个就停止)。
Q:Stream 和 for 循环哪个性能更好?
在单线程、数据量小的场景,for 循环更快——Stream 有创建流水线、封装/拆封装基本类型、虚方法调用等额外开销。在多线程、数据量大的场景,
parallelStream的多核加速可能显著趁过 for 循环;但即使如此也受任务划分成本与 ForkJoinPool 共享者影响,严谨对比请用 JMH 自测。遵从的原则:优先选择可读性更好的写法,通常是 Stream;只有在证实为热点的紧密循环中再考虑改为 for。
Q:parallelStream 一定比 stream 快吗?
不一定。parallelStream 使用 ForkJoinPool 多线程执行,适合 CPU 密集型、数据量大的场景。如果任务本身很轻量或数据量小,线程切换的开销反而会使性能更差。
3. Optional¶
3.1 引入:它解决了什么问题?¶
Optional 将"可能为空"这个语义显式化,强制调用方处理空值情况,减少 NullPointerException。
// 传统写法:层层判空,代码丑陋,容易遗漏
String city = null;
if (user != null) {
Address address = user.getAddress();
if (address != null) {
city = address.getCity();
}
}
// Optional 写法:链式调用,语义清晰,强制处理空值
String city = Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCity)
.orElse("未知城市");
为什么 Optional 能减少 NPE:Optional 将"可能为空"这个语义显式化,调用方必须处理空值情况(通过
orElse/ifPresent等),而不是忘记判空。
3.2 Optional 核心 API¶
// 创建
Optional<String> opt1 = Optional.of("value"); // 值不能为null,否则NPE
Optional<String> opt2 = Optional.ofNullable(null); // 值可以为null
Optional<String> opt3 = Optional.empty(); // 空Optional
// 判断与获取
opt1.isPresent(); // true
opt1.get(); // "value"(为空时抛NoSuchElementException)
opt2.orElse("default"); // null时返回"default"
opt2.orElseGet(() -> computeDefault()); // 懒加载默认值(比orElse更高效,只在为空时才计算)
opt2.orElseThrow(() -> new RuntimeException("值不存在"));
// 转换
opt1.map(String::toUpperCase); // Optional["VALUE"]
opt1.filter(s -> s.length() > 3); // Optional["value"]
opt1.flatMap(s -> Optional.of(s + "!")); // Optional["value!"]
为什么
orElseGet比orElse更高效:orElse("default")无论是否为空都会计算默认值;orElseGet(() -> computeDefault())只在为空时才执行 Lambda,如果默认值计算开销大(如查数据库),应优先用orElseGet。
3.3 Optional 使用原则(误区分析)¶
// ✅ 正确:用于方法返回值,表达"可能没有结果"
public Optional<User> findUserById(Long id) {
return Optional.ofNullable(userRepository.findById(id));
}
// ❌ 错误:不应用于方法参数(调用方传null更混乱)
// 原因:调用方可能传 Optional.empty(),也可能传 null,反而增加处理复杂度
public void process(Optional<String> name) { ... }
// ❌ 错误:不应用于字段(序列化问题)
// 原因:Optional 没有实现 Serializable,序列化时会报错
public class User {
private Optional<String> nickname; // 错误!
}
// ❌ 错误:不要用isPresent() + get(),等同于判null,没有意义
if (opt.isPresent()) {
String val = opt.get(); // 这和 if(val != null) 没区别,失去了 Optional 的意义
}
// ✅ 应该用 ifPresent() 或 map()
opt.ifPresent(val -> System.out.println(val));
3.4 Optional 常见问题¶
Q:Optional 为什么不能用于方法参数?
如果方法参数是
Optional<T>,调用方可能传入Optional.empty(),也可能传入null(忘记包装),反而增加了处理复杂度。方法参数应该直接用@Nullable注解或重载方法来表达可选性。
Q:为什么 orElseGet 比 orElse 更高效?
orElse(value)无论是否为空都会计算 value;orElseGet(() -> compute())只在为空时才执行 Lambda。如果默认值计算开销大(如查数据库),应优先用orElseGet。
Q:Optional.of() 和 Optional.ofNullable() 有什么区别?
Optional.of(value)要求 value 不能为 null,否则立即抛 NullPointerException;Optional.ofNullable(value)允许 value 为 null,为 null 时返回Optional.empty()。
3.5 Optional 工作中常见坑¶
❌ 坑1:Optional 嵌套(Optional 套 Optional)¶
// ❌ 错误:返回 Optional<Optional<User>>,调用方很难处理
public Optional<Optional<User>> findUser(Long id) {
Optional<User> user = userRepo.findById(id);
return Optional.of(user); // 多此一举!
}
// ✅ 正确:直接返回 Optional<User>
public Optional<User> findUser(Long id) {
return userRepo.findById(id);
}
// ❌ 错误:flatMap 用错成 map,导致嵌套
Optional<String> city = Optional.ofNullable(user)
.map(u -> Optional.ofNullable(u.getAddress())) // 返回 Optional<Optional<Address>>
.map(addr -> addr.map(Address::getCity)); // 很难用
// ✅ 正确:用 flatMap 展开
Optional<String> city = Optional.ofNullable(user)
.flatMap(u -> Optional.ofNullable(u.getAddress())) // 返回 Optional<Address>
.map(Address::getCity);
❌ 坑2:在 Stream 中与 Optional 配合使用¶
// 场景:从 ID 列表中查找用户,过滤掉不存在的
List<Long> ids = Arrays.asList(1L, 2L, 3L);
// ❌ 错误:map 返回 Optional,collect 得到 List<Optional<User>>
List<Optional<User>> users = ids.stream()
.map(id -> userRepo.findById(id))
.collect(Collectors.toList());
// ✅ 正确:用 flatMap 展开 Optional(Java 9+)
List<User> users = ids.stream()
.map(id -> userRepo.findById(id)) // Stream<Optional<User>>
.flatMap(Optional::stream) // 展开:空 Optional 被过滤,有值的展开为元素
.collect(Collectors.toList());
// Java 8 的写法(没有 Optional::stream)
List<User> users = ids.stream()
.map(id -> userRepo.findById(id))
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
❌ 坑3:Optional 序列化问题¶
// ❌ 错误:Optional 没有实现 Serializable,作为字段会导致序列化失败
public class UserDTO implements Serializable {
private Optional<String> nickname; // 序列化时报错!
}
// ✅ 正确:字段用普通类型,在 getter 中返回 Optional
public class UserDTO {
private String nickname; // 字段用普通类型
public Optional<String> getNickname() {
return Optional.ofNullable(nickname); // getter 返回 Optional
}
}