写在前面:这是JDK8新特性系列的最后一篇。经过前面11天的学习,我们已经掌握了Lambda、Stream、Optional、日期时间API等核心特性。今天这篇汇总文章,我会通过真实项目案例把这些知识点串起来,并整理出面试中最常问的问题。建议收藏,面试前反复看。

文章目录
-
- 一、JDK8新特性全景回顾
-
- [1.1 核心特性一览](#1.1 核心特性一览)
- 二、Lambda+Stream实战案例
-
- [2.1 员工信息统计案例](#2.1 员工信息统计案例)
- [2.2 Stream调试技巧](#2.2 Stream调试技巧)
- [2.3 并行流的线程安全问题](#2.3 并行流的线程安全问题)
- 三、Optional实战案例
-
- [3.1 避免深层null检查](#3.1 避免深层null检查)
- [3.2 orElse vs orElseGet的区别](#3.2 orElse vs orElseGet的区别)
- [3.3 Optional在Repository层的应用](#3.3 Optional在Repository层的应用)
- 四、日期时间API实战案例
-
- [4.1 计算年龄](#4.1 计算年龄)
- [4.2 计算工作日](#4.2 计算工作日)
- [4.3 时区转换](#4.3 时区转换)
- 五、CompletableFuture实战案例
-
- [5.1 并行查询用户数据](#5.1 并行查询用户数据)
- [5.2 异步任务异常处理](#5.2 异步任务异常处理)
- 六、面试高频考点汇总
-
- [6.1 Lambda表达式](#6.1 Lambda表达式)
- [6.2 Stream流](#6.2 Stream流)
- [6.3 Optional](#6.3 Optional)
- [6.4 日期时间API](#6.4 日期时间API)
- [6.5 CompletableFuture](#6.5 CompletableFuture)
- 七、JDK8升级注意事项
-
- [7.1 兼容性问题](#7.1 兼容性问题)
- [7.2 性能注意事项](#7.2 性能注意事项)
- 八、学习路线总结
-
- [8.1 学习阶段建议](#8.1 学习阶段建议)
- [8.2 实践建议](#8.2 实践建议)
- 参考资料
一、JDK8新特性全景回顾
实际场景:面试官问:"说说你在项目中用过哪些JDK8特性?"如果你只能答出"用过Lambda",那分数肯定不高。你需要展示出对各个特性的深入理解和实际应用经验。
1.1 核心特性一览
JDK8带来了8大类新特性:
| 特性类别 | 核心内容 | 使用频率 | 面试频率 |
|---|---|---|---|
| Lambda表达式 | 简化匿名内部类 | ★★★★★ | ★★★★★ |
| Stream流 | 声明式数据处理 | ★★★★★ | ★★★★★ |
| Optional | 避免空指针 | ★★★★☆ | ★★★★☆ |
| 日期时间API | 替代Date/Calendar | ★★★★☆ | ★★★☆☆ |
| CompletableFuture | 异步编程 | ★★★☆☆ | ★★★★☆ |
| 接口默认方法 | 接口演化 | ★★★☆☆ | ★★★☆☆ |
| 方法引用 | 进一步简化 | ★★★★☆ | ★★☆☆☆ |
| 新工具类 | Objects/String/Arrays等 | ★★★★☆ | ★★☆☆☆ |
经验之谈:在实际项目中,Lambda+Stream的组合使用频率最高,几乎每天都会用到。CompletableFuture在微服务架构中非常重要,但很多人只停留在会用的层面,对原理和坑点了解不深。
二、Lambda+Stream实战案例
2.1 员工信息统计案例
实际场景:HR系统需要统计员工数据,包括部门平均薪资、薪资排名、年龄段分布等。传统写法需要多层循环和条件判断,代码冗长且容易出错。
Stream解决方案:
java
// 按部门分组,计算平均薪资
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
// 找出每个部门薪资最高的员工
Map<String, Optional<Employee>> topByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.maxBy(Comparator.comparing(Employee::getSalary))
));
// 获取薪资排名前3的员工
List<String> top3Names = employees.stream()
.sorted(Comparator.comparing(Employee::getSalary).reversed())
.limit(3)
.map(Employee::getName)
.collect(Collectors.toList());
踩坑提醒 :sorted()操作会触发全量排序,如果数据量很大且只需要前N个,应该先用filter缩小范围,或者使用Stream的惰性特性配合limit。
2.2 Stream调试技巧
实际场景:Stream链式调用很长,出了问题不知道在哪一步出错。
使用peek进行调试:
java
List<String> result = list.stream()
.peek(e -> System.out.println("处理前: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("转换后: " + e))
.filter(s -> s.length() > 3)
.collect(Collectors.toList());
经验之谈 :peek是专门用于调试的中间操作,它不会改变流中的元素,只是对每个元素执行一个操作。生产环境的代码应该去掉peek,避免影响性能。
2.3 并行流的线程安全问题
踩坑提醒 :很多人以为用了parallelStream()就能提升性能,结果代码反而更慢了,甚至出现数据不一致的问题。
错误示范:
java
// 错误:并发修改共享变量
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
int sum = 0;
list.parallelStream().forEach(i -> sum += i); // 结果不确定!
正确做法:
java
// 正确:使用reduce进行归约
int sum = list.parallelStream()
.mapToInt(Integer::intValue)
.sum();
// 或者使用collect
List<String> result = list.parallelStream()
.map(Object::toString)
.collect(Collectors.toList());
经验之谈 :并行流适合"大数据量+复杂计算"的场景。数据量小于1万或者计算很简单时,串行流反而更快。另外,并行流默认使用ForkJoinPool.commonPool,线程数等于CPU核心数,如果任务会阻塞(如IO操作),可能会占满线程池影响其他功能。
三、Optional实战案例
3.1 避免深层null检查
实际场景:获取用户的所在城市名称,传统写法需要层层判断null,代码臃肿。
传统写法的问题:
java
// 这种写法容易漏判null,而且可读性差
if (user != null && user.getAddress() != null
&& user.getAddress().getCity() != null) {
return user.getAddress().getCity().getName();
}
return "Unknown";
Optional优雅写法:
java
return Optional.ofNullable(user)
.flatMap(User::getAddress)
.flatMap(Address::getCity)
.map(City::getName)
.orElse("Unknown");
经验之谈 :flatMap用于链式调用中每个节点都可能为null的场景。如果中间某个节点返回null,整个链会返回Optional.empty(),不会抛出NPE。
3.2 orElse vs orElseGet的区别
踩坑提醒 :很多人混用orElse和orElseGet,结果在不需要默认值的情况下也执行了昂贵的计算。
java
// orElse:无论是否需要,都会执行参数
String result = Optional.of("value")
.orElse(getDefaultFromDatabase()); // 数据库查询会被执行
// orElseGet:仅在需要时才执行(推荐)
String result = Optional.of("value")
.orElseGet(() -> getDefaultFromDatabase()); // 不会执行
经验之谈 :如果默认值的获取有性能开销(如数据库查询、复杂计算),一定要用orElseGet。只有当默认值是常量时,才用orElse。
3.3 Optional在Repository层的应用
实际场景:DAO层查询数据库,结果可能不存在,返回Optional比返回null更安全。
java
@Repository
public class UserRepository {
public Optional<User> findById(Long id) {
User user = entityManager.find(User.class, id);
return Optional.ofNullable(user);
}
}
// Service层使用
public User getUser(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
}
踩坑提醒 :不要用Optional作为方法参数或集合元素类型。这会增加不必要的包装开销,而且调用方可能传入null,失去Optional的意义。
四、日期时间API实战案例
4.1 计算年龄
实际场景:用户注册时需要判断是否成年,或者显示用户的精确年龄。
java
// 计算周岁
public static int calculateAge(LocalDate birthDate) {
return Period.between(birthDate, LocalDate.now()).getYears();
}
// 计算精确年龄
public static String calculateAgeDetailed(LocalDate birthDate) {
Period period = Period.between(birthDate, LocalDate.now());
return String.format("%d岁%d个月%d天",
period.getYears(), period.getMonths(), period.getDays());
}
// 计算距离下次生日的天数
public static long daysUntilNextBirthday(LocalDate birthDate) {
LocalDate today = LocalDate.now();
LocalDate nextBirthday = birthDate.withYear(today.getYear());
if (!nextBirthday.isAfter(today)) {
nextBirthday = nextBirthday.plusYears(1);
}
return ChronoUnit.DAYS.between(today, nextBirthday);
}
经验之谈 :Period适合计算年月日,ChronoUnit适合计算总天数/小时数等。不要用Period计算总天数,因为它不考虑月份天数差异。
4.2 计算工作日
实际场景:项目管理系统需要计算任务的工作日工期,排除周末和节假日。
java
public static long calculateWorkdays(LocalDate start, LocalDate end) {
return start.datesUntil(end.plusDays(1))
.filter(date -> {
DayOfWeek dow = date.getDayOfWeek();
return dow != DayOfWeek.SATURDAY
&& dow != DayOfWeek.SUNDAY
&& !HOLIDAYS.contains(date);
})
.count();
}
踩坑提醒 :datesUntil是JDK9新增的方法,JDK8需要用Stream.iterate实现。另外,节假日判断要考虑调休,实际项目中建议从配置中心或数据库读取节假日配置。
4.3 时区转换
实际场景:国际化系统需要处理不同时区的时间显示。
java
// 北京时间转纽约时间
ZonedDateTime beijingTime = LocalDateTime.now().atZone(ZoneId.of("Asia/Shanghai"));
ZonedDateTime nyTime = beijingTime.withZoneSameInstant(ZoneId.of("America/New_York"));
// 存储时使用UTC,显示时转换为用户时区
Instant utc = Instant.now();
ZonedDateTime userTime = utc.atZone(ZoneId.of(user.getTimeZone()));
经验之谈 :数据库中存储时间建议用TIMESTAMP类型(存储UTC时间),业务层根据用户时区进行转换。不要用LocalDateTime存储带时区的时间,因为它不包含时区信息。
五、CompletableFuture实战案例
5.1 并行查询用户数据
实际场景:用户仪表盘需要同时显示用户信息、订单列表、推荐商品、账户余额,每个查询都是独立的,串行执行太慢。
java
public String getUserDashboard(Long userId) {
long start = System.currentTimeMillis();
CompletableFuture<String> userInfo = getUserInfo(userId);
CompletableFuture<String> orders = getUserOrders(userId);
CompletableFuture<String> recommendations = getRecommendations(userId);
CompletableFuture<Double> balance = getBalance(userId);
// 等待所有任务完成
CompletableFuture.allOf(userInfo, orders, recommendations, balance).join();
// 获取结果(此时都已就绪,不会阻塞)
String result = String.format("%s | %s | %s | 余额: %.2f",
userInfo.get(), orders.get(), recommendations.get(), balance.get());
System.out.println("耗时: " + (System.currentTimeMillis() - start) + "ms");
return result;
}
经验之谈 :allOf返回的CompletableFuture在所有任务完成后完成,但本身不携带结果。需要用get()或join()获取每个任务的结果。join()和get()的区别是join不抛出受检异常。
5.2 异步任务异常处理
踩坑提醒:异步任务中的异常不会自动抛出,如果不处理,会导致结果异常或永远等待。
java
// 错误:异常被吞掉,返回null
String result = CompletableFuture.supplyAsync(() -> {
if (userId < 0) throw new IllegalArgumentException("无效ID");
return "结果";
}).join(); // 如果异常,这里返回null
// 正确:使用exceptionally处理异常
String result = CompletableFuture.supplyAsync(() -> {
if (userId < 0) throw new IllegalArgumentException("无效ID");
return "结果";
})
.exceptionally(ex -> {
System.out.println("异常: " + ex.getMessage());
return "默认值";
})
.join();
经验之谈 :exceptionally相当于try-catch,handle相当于try-catch-finally(能同时处理正常结果和异常)。如果需要在异常时返回降级结果,用exceptionally;如果需要统一处理无论成功失败,用handle。
六、面试高频考点汇总
6.1 Lambda表达式
Q1:Lambda表达式和匿名内部类的区别?
答案要点:
- this指向不同:Lambda指向外部类,匿名内部类指向自身
- 编译方式不同:Lambda用invokedynamic,匿名内部类生成.class文件
- 变量捕获:Lambda只能访问effectively final变量
Q2:什么是函数式接口?
答案 :只有一个抽象方法的接口,可以用@FunctionalInterface注解。JDK8内置的四大核心函数式接口是Function<T,R>、Consumer<T>、Supplier<T>、Predicate<T>。
6.2 Stream流
Q3:Stream和Collection的区别?
答案:
- Collection是数据容器,存储数据
- Stream是计算管道,不存储数据,不改变源数据,惰性执行
Q4:parallelStream的线程安全问题?
答案:
- 不要在并行流中修改共享变量
- 不要用线程不安全的集合收集结果
- 使用
reduce或collect进行归约操作
Q5:Stream的性能优化建议?
答案:
- 先filter再map,减少计算量
- 使用基本类型流(IntStream/LongStream)避免装箱
- 数据量小或计算简单时不使用并行流
- 使用短路操作(findFirst/anyMatch)提前终止
6.3 Optional
Q6:Optional的正确使用方式?
答案:
- 作为方法返回值,表示可能为空
- 使用
orElseGet替代orElse获取延迟计算的默认值 - 使用
flatMap进行链式调用避免NPE - 不要 :作为方法参数、作为集合元素、直接调用
get()
Q7:orElse和orElseGet的区别?
答案 :orElse无论是否需要都会执行参数,orElseGet只在需要时执行。默认值有性能开销时用orElseGet。
6.4 日期时间API
Q8:新的日期时间API相比Date/Calendar有什么优势?
答案:
- 不可变对象,线程安全
- API设计清晰,区分日期、时间、时区
- 支持 fluent API,链式调用
- 时区处理更方便
Q9:DateTimeFormatter是线程安全的吗?
答案 :是的。DateTimeFormatter是不可变的,可以安全地共享。而SimpleDateFormat不是线程安全的,多线程环境下要使用ThreadLocal。
6.5 CompletableFuture
Q10:CompletableFuture和Future的区别?
答案:
- Future只能获取结果,不能组合、链式调用
- CompletableFuture支持回调、组合、异常处理、超时控制
Q11:如何处理CompletableFuture的异常?
答案:
exceptionally:异常时返回默认值handle:统一处理正常结果和异常whenComplete:副作用处理,不改变结果
七、JDK8升级注意事项
7.1 兼容性问题
接口默认方法冲突:
如果一个类实现了两个接口,且两个接口有同名的默认方法,必须重写该方法并显式调用父接口的实现。
java
interface A { default void hello() { System.out.println("A"); } }
interface B { default void hello() { System.out.println("B"); } }
class C implements A, B {
@Override
public void hello() {
A.super.hello(); // 显式调用
}
}
7.2 性能注意事项
经验之谈:
- 大数据量+复杂计算才用并行流
- 避免在Stream中自动装箱拆箱
- 使用短路操作提前终止
- 注意CompletableFuture默认线程池的大小限制
八、学习路线总结
8.1 学习阶段建议
| 阶段 | 重点内容 | 建议练习 |
|---|---|---|
| 入门 | Lambda语法、Stream基础操作 | 改造现有代码中的循环 |
| 进阶 | Stream收集器、Optional链式调用 | 复杂业务场景实战 |
| 高级 | 并行流、CompletableFuture | 性能优化、异步编程 |
| 实战 | 综合运用 | 项目重构、代码Review |
8.2 实践建议
- 从小处着手:先在工具类中使用Lambda和Stream
- 注重可读性:Stream链不宜过长,适当拆分
- 性能意识:了解并行流的使用场景
- 代码审查:关注Optional滥用、装箱拆箱等问题
参考资料
互动话题:学完整个JDK8系列,你最常用的是哪个特性?在实际项目中遇到过哪些坑?欢迎在评论区分享你的经验!
如果这篇文章对你有帮助,欢迎点赞、收藏 !这是【JDK8新特性】系列的收官之作,关注我看更多Java技术文章 👇
本文为【JDK8新特性】系列第12篇,系列完结。