JDK8 新特性实战(Lambda / Stream / Optional)
对应学习计划:第一月第1周「Java 基础 + 集合源码」
面试定位:JDK8 是后端开发的事实标准,Lambda/Stream/Optional 是必须熟练的基础能力,几乎每次面试都会被问到。
一、本文能帮你回答的面试题
- Lambda 表达式的本质是什么?为什么说它只是「语法糖」?
@FunctionalInterface是什么?哪些是 JDK 自带的函数式接口?- Stream 的中间操作和终止操作有什么区别?
map和flatMap区别? - Stream 为什么不能重复使用?
- Optional 是什么?怎么用?什么时候不该用?
- 方法引用有哪四种形式?
- 接口默认方法(default method)解决了什么问题?
二、概念介绍
JDK8 于 2014 年发布,是 Java 历史上最重要的一次版本升级,核心变化包括:
| 新特性 | 作用 |
|---|---|
| Lambda 表达式 | 把「匿名内部类」的样板代码压缩为一行 |
| 函数式接口 | Lambda 的「类型」载体 |
| Stream API | 用声明式方式处理集合,支持链式调用 + 并行流 |
| Optional | 优雅处理空指针(NPE) |
| 方法引用 | :: 语法,进一步简化 Lambda |
| 接口默认方法/静态方法 | 给接口增加方法而不破坏实现类 |
| 新时间 API | LocalDate/LocalDateTime 替代 Date/Calendar |
| CompletableFuture | 异步编程(第10篇详细讲) |
一句话总结:JDK8 让 Java 从「面向对象的语言」进化为「也能写出函数式风格的代码」的语言。
三、原理深入
3.1 Lambda 的本质:是语法糖,不是新指令
java
// 写法1:传统匿名内部类
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
// 写法2:Lambda(等价)
Runnable r2 = () -> System.out.println("Hello");
原理 :Lambda 表达式编译后会被转换为 invokedynamic 指令 + 生成一个内部的函数式接口实现,最终仍是一个对象。它没有引入新的字节码指令,所以叫「语法糖」。
3.2 函数式接口:Lambda 的「身份证」
Lambda 能用的前提:接口中只有一个抽象方法 。用 @FunctionalInterface 注解标记(不加也能用,加了会在编译期校验)。
JDK8 自带四大核心函数式接口(背下来):
| 接口 | 参数 | 返回值 | 典型场景 |
|---|---|---|---|
Predicate<T> |
T | boolean | 过滤条件 |
Function<T,R> |
T | R | 类型转换 |
Consumer<T> |
T | void | 遍历消费 |
Supplier<T> |
无 | T | 工厂创建对象 |
3.3 Stream 两段式设计:中间操作 vs 终止操作
数据源 (集合/数组) → 中间操作链 (惰性) → 终止操作 (触发执行)
- 中间操作 (lazy):
filter / map / flatMap / sorted / distinct / limit / skip,不执行,只是记录到流水线。 - 终止操作 (eager):
forEach / collect / count / reduce / anyMatch / findFirst,一调用就触发整条流水线执行。
为什么这么设计?
- 惰性求值可以避免不必要的中间结果(像 SQL 执行计划一样优化)。
- 全部在终止操作的时候一次遍历完成,减少遍历次数。
⚠️ 坑 :没有终止操作的 Stream 不会执行,写
list.stream().filter(x -> x > 3)是不生效的。
3.4 map vs flatMap
java
// map:一对一映射,结果是 List<String[]>
List<String[]> m = words.stream()
.map(w -> w.split(""))
.collect(Collectors.toList());
// flatMap:一对多展平,结果是 List<String>
List<String> fm = words.stream()
.flatMap(w -> Arrays.stream(w.split("")))
.collect(Collectors.toList());
记忆口诀 :map 结果是「一个进一个出」,flatMap 结果是「一个进、多个出且被摊平」。
3.5 Optional:用类型系统消灭 NPE
java
// 传统写法:多层 null 判断,容易漏
public String getCity(User user) {
if (user != null) {
Address addr = user.getAddress();
if (addr != null) {
return addr.getCity();
}
}
return "未知";
}
// Optional 链式写法:不优雅地兜底
public String getCity(User user) {
return Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCity)
.orElse("未知");
}
Optional 常用 API:
| 方法 | 作用 |
|---|---|
of(obj) |
包非空值,为 null 直接抛 NPE |
ofNullable(obj) |
可包装 null 值 |
orElse(v) |
为空返回默认值(无论空不空都会先计算) |
orElseGet(() -> v) |
为空才执行 Supplier 求值(推荐,惰性) |
orElseThrow(Supplier) |
为空抛自定义异常 |
map() / flatMap() |
值转换 |
ifPresent() / filter() |
条件消费 / 过滤 |
⚠️ 面试加分点(装逼) :
orElse和orElseGet的区别 ------orElse的参数是已经求值好的对象 ,即使 Optional 非空也会执行;orElseGet是 Supplier,只在为空时求值。性能敏感场景(如创建昂贵对象)必须用orElseGet。
3.6 方法引用四种形式
| 形式 | 语法 | 示例 |
|---|---|---|
| 静态方法 | 类名::静态方法 |
Math::max |
| 实例方法(特定对象) | 对象::实例方法 |
System.out::println |
| 任意对象实例方法 | 类名::实例方法 |
String::toUpperCase |
| 构造器 | 类名::new |
ArrayList::new |
3.7 接口 default 方法:多继承问题的折中
java
interface Vehicle {
void run();
// 新加的方法不影响已有实现类
default void honk() { System.out.println("滴滴"); }
static void info() { System.out.println("我是车"); }
}
解决的问题 :给接口加新方法(如集合的 forEach)时,不用强制所有实现类修改。
引入的问题 :接口默认方法多继承会有歧义,需要 X.super.方法() 显式指定。
四、实战例子
实战 1:用 Stream 完成「按年龄分组统计」
java
import java.util.*;
import java.util.stream.*;
public class StreamDemo {
record Person(String name, int age, String city) {}
public static void main(String[] args) {
List<Person> people = List.of(
new Person("张三", 18, "北京"),
new Person("李四", 25, "上海"),
new Person("王五", 30, "北京"),
new Person("赵六", 25, "深圳"));
// 1. 按城市分组
Map<String, List<Person>> byCity = people.stream()
.collect(Collectors.groupingBy(Person::city));
System.out.println(byCity);
// 2. 过滤年龄 >= 25,转成名字列表,按名字排序
List<String> names = people.stream()
.filter(p -> p.age() >= 25)
.map(Person::name)
.sorted()
.collect(Collectors.toList());
System.out.println(names);
// 3. 统计平均年龄
double avg = people.stream()
.mapToInt(Person::age)
.average()
.orElse(0);
System.out.println("平均年龄: " + avg);
// 4. 取年龄最大的前2个人
List<Person> top2 = people.stream()
.sorted(Comparator.comparingInt(Person::age).reversed())
.limit(2)
.collect(Collectors.toList());
System.out.println(top2);
}
}
实战 2:Optional 实战------用户服务
java
public class UserService {
// 模拟数据库查询,可能返回 null
public User findById(Long id) { return null; }
// 推荐写法:链式 + 兜底 + 显式异常
public AddressDto getAddress(Long userId) {
return Optional.ofNullable(findById(userId))
.map(User::getAddress)
.map(a -> new AddressDto(a.getCity(), a.getStreet()))
.orElseThrow(() -> new RuntimeException("用户地址不存在: " + userId));
}
// 集合操作里替代 for + if null
public List<String> getCityList(List<User> users) {
return users.stream()
.filter(Objects::nonNull)
.map(User::getAddress)
.filter(Objects::nonNull) // 防止 Address 为 null
.map(Address::getCity)
.distinct()
.collect(Collectors.toList());
}
}
实战 3:并行流(Parallel Stream)
java
// 大数据量计算用并行流,内部走 ForkJoinPool 公共池
long sum = LongStream.rangeClosed(1, 10_000_000L)
.parallel() // 注意:list.parallelStream() 也行
.sum();
System.out.println(sum);
⚠️ 坑 :并行流共享全局 ForkJoinPool,遇到 IO 阻塞任务会堵死其他并行流;数据量小的时候并行反而更慢(有切换开销)。不要在业务中盲目用 parallel()。
五、面试话术(背下来)
Q:Lambda 为什么快/是什么?
「Lambda 是一个语法糖,本质是编译成 invokedynamic 指令,把匿名内部类转换成 JVM 能惰性生成的函数式接口实现,避免每次创建匿名内部类都生成一个类文件,减少类加载和对象创建开销。」
Q:Stream 中间操作和终止操作的区别?
「中间操作是惰性的,只是把操作记在流水线上不执行;终止操作一调用,整条流水线一次性触发。好处一是能优化执行计划避免多余遍历,二是可以短路优化,比如 limit(10) 找到 10 个就停止。」
Q:怎么防 NPE?
「能用 Optional.ofNullable 链式处理就用,避免多层 if null;对象字段设计上尽量不让它为空,用 orElseGet 做惰性兜底。」
Q:orElse 和 orElseGet?
「orElse 参数已求值好的对象,无论 Optional 空不空都执行创建;orElseGet 传 Supplier,只有为空时才创建。创建昂贵对象必须用 orElseGet。」
Q:并行流注意什么?
「并行流基于公共 ForkJoinPool,占用 IO 的任务会互相影响;数据量小并行更慢;线程安全也要自己保证。不建议在 Web 应用里无脑 parallelStream。」
六、踩坑总结(易错点)
| # | 坑 | 说明 |
|---|---|---|
| 1 | 忘写终止操作 | Stream 不会执行,代码「看起来对」但没跑 |
| 2 | Stream 不可复用 | stream.forEach(...) 后 stream 已关闭,再操作抛 IllegalStateException |
| 3 | orElse 提前求值 |
传 new Object() 会无条件创建对象 |
| 4 | 并行流乱用 | 公共池互相干扰 + 数据量大才有收益 |
| 5 | Lambda 里修改外部变量 | 外部局部变量必须 final 或 effectively final |
| 6 | map 后仍有 null |
map 不自动判空,结果可能为「null 值的元素」,接 filter(Objects::nonNull) |
| 7 | groupingBy 的 key 为 null | Collectors.groupingBy 默认 key 不能为 null,会 NPE |
七、课后作业
- 用
groupingBy+counting()统计一个单词列表的词频,并输出 Top3。 - 用自己的话解释为什么
Stream只能消费一次(提示:Spliterator状态)。 - 用
reduce手写一个「求最大数」的聚合操作,对比max()的写法差异。 - 项目里找一个 for 循环,改写成 Stream,对比可读性。