JDK 7、JDK 8 与后续版本差异
Java 学习不能只看最新版本。大量企业项目仍运行在 JDK 8,很多老系统甚至还在 JDK 7。面试也经常追问“JDK 7 和 JDK 8 有什么区别”“HashMap 在 JDK 7 和 JDK 8 有什么变化”“为什么 JDK 8 引入 Lambda”。所以学习 JavaSE 要按版本演进理解,而不是只背 API。
学习目标
学完本章要能回答:
- JDK 7、JDK 8、JDK 11、JDK 17、JDK 21 常见差异是什么。
- 为什么 JDK 8 是企业 Java 的关键分水岭。
- JDK 7 和 JDK 8 在集合、并发、接口、日期时间、函数式编程上有什么区别。
- 新版本特性在老项目中不能用时,应该如何替代。
- 面试被问版本差异时,如何从“语言、集合、并发、JVM、工程化”几个角度回答。
版本演进主线
flowchart TD
A["JDK 7:老项目基础"] --> B["try-with-resources、switch String、菱形语法"]
B --> C["JDK 8:企业主流分水岭"]
C --> D["Lambda、Stream、Optional、java.time、CompletableFuture"]
D --> E["JDK 9 到 11:模块化和工程增强"]
E --> F["JDK 17:现代长期支持版本"]
F --> G["JDK 21 及之后:虚拟线程、模式匹配等现代能力"]可以这样记:JDK 7 是传统 Java 写法的成熟期,JDK 8 是函数式和现代 API 的分水岭,JDK 11/17/21 是长期支持版本的演进线。
JDK 7 为什么仍然重要
JDK 7 在很多老系统里还能见到,特别是历史较久的政企系统、传统单体系统、早期 Spring MVC 项目。它没有 Lambda,也没有 Stream,更没有 java.time,所以代码风格通常更“命令式”。
JDK 7 重要特性:
| 特性 | 解决的问题 | 示例 |
|---|---|---|
| switch 支持 String | 不再只能对 int、enum 做 switch | switch (type) |
| try-with-resources | 自动关闭资源,减少 finally 样板代码 | 关闭文件流、连接 |
| 菱形语法 | 泛型实例化少写重复类型 | new ArrayList<>() |
| 多异常捕获 | 一个 catch 捕获多个异常 | `catch (IOException |
| NIO.2 | 更好的文件 API | Path、Files |
JDK 7 写法 Demo
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class Jdk7TryWithResourcesDemo {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
System.out.println(reader.readLine());
} catch (IOException e) {
System.out.println("读取文件失败:" + e.getMessage());
}
}
}这里的重点不是文件读取,而是资源关闭。JDK 7 以前通常要在 finally 中手动关闭资源,容易忘记关闭或关闭异常覆盖业务异常。try-with-resources 让实现了 AutoCloseable 的资源自动关闭。
JDK 8 为什么最重要
JDK 8 是企业开发最重要的版本之一,因为它同时改变了代码写法、集合处理方式、日期时间 API、异步编程方式和接口设计方式。
JDK 8 核心特性:
| 特性 | 重要原因 | 典型场景 |
|---|---|---|
| Lambda | 把行为当参数传递,减少匿名内部类 | 排序、回调、策略 |
| Stream | 声明式处理集合 | 过滤、映射、分组、统计 |
| Optional | 显式表达“可能没有值” | 查询结果、外部接口返回 |
| java.time | 替代 Date/Calendar | 订单时间、账单周期、过期时间 |
| 接口默认方法 | 接口可以平滑扩展 | 框架接口兼容 |
| CompletableFuture | 组合异步任务 | 接口聚合、并行查询 |
| HashMap 红黑树优化 | 降低极端哈希冲突性能退化 | 大量 key 冲突场景 |
| ConcurrentHashMap 重构 | 从分段锁走向 CAS + synchronized + 桶级锁 | 高并发 Map |
JDK 7 和 JDK 8 写法对比
集合过滤
JDK 7 常见写法:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Jdk7FilterDemo {
public static void main(String[] args) {
List<String> names = Arrays.asList("tom", "jerry", "tony");
List<String> result = new ArrayList<String>();
for (String name : names) {
if (name.startsWith("t")) {
result.add(name);
}
}
System.out.println(result);
}
}JDK 8 写法:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Jdk8StreamDemo {
public static void main(String[] args) {
List<String> names = Arrays.asList("tom", "jerry", "tony");
List<String> result = names.stream()
.filter(name -> name.startsWith("t"))
.collect(Collectors.toList());
System.out.println(result);
}
}JDK 8 写法更短,但不能理解成“Stream 一定更快”。Stream 的价值是表达数据处理流程,性能要看数据量、操作复杂度、是否并行、是否产生装箱拆箱。
HashMap:JDK 7 和 JDK 8 区别
这是面试高频点。
flowchart TD
A["HashMap put"] --> B{"数组位置是否为空"}
B -- "空" --> C["直接放入节点"]
B -- "不空" --> D["发生哈希冲突"]
D --> E{"JDK 7"}
D --> F{"JDK 8"}
E --> G["数组 + 链表,头插法"]
F --> H["数组 + 链表 + 红黑树,尾插法"]
H --> I{"链表长度和数组容量达到阈值"}
I -- "满足" --> J["链表树化"]核心区别:
| 对比 | JDK 7 | JDK 8 |
|---|---|---|
| 数据结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
| 插入方式 | 头插法 | 尾插法 |
| 扩容迁移 | 多线程下可能形成环 | 优化迁移逻辑,但仍非线程安全 |
| 极端冲突 | 链表过长退化为 O(n) | 树化后接近 O(log n) |
| 并发安全 | 不安全 | 仍然不安全 |
为什么 JDK 8 加红黑树:如果大量 key 哈希冲突,链表查询会从接近 O(1) 退化为 O(n)。红黑树能降低极端情况下的查询成本。但这不代表业务可以忽略 hashCode 质量,也不代表 HashMap 可以并发写。
ConcurrentHashMap:JDK 7 和 JDK 8 区别
flowchart TD
A["ConcurrentHashMap"] --> B{"JDK 7"}
A --> C{"JDK 8"}
B --> D["Segment 分段锁"]
D --> E["每个 Segment 类似小 HashMap"]
C --> F["数组 + 链表 + 红黑树"]
F --> G["CAS 写空桶"]
F --> H["桶级 synchronized"]
F --> I["多线程协助扩容"]| 对比 | JDK 7 | JDK 8 |
|---|---|---|
| 锁粒度 | Segment 级别 | 桶级别 |
| 底层结构 | Segment + HashEntry | Node 数组 + 链表 + 红黑树 |
| 写空桶 | Segment 加锁 | CAS |
| 冲突写入 | Segment 加锁 | synchronized 锁桶头 |
| 扩容 | Segment 内扩容 | 支持多线程协助迁移 |
为什么 JDK 8 不再使用 Segment:Segment 结构让并发级别和内部结构更复杂,JDK 8 通过 CAS、volatile、桶级锁和红黑树降低锁粒度,也让结构更接近 HashMap。
接口:JDK 7 和 JDK 8 区别
JDK 7 中接口只能定义常量和抽象方法。JDK 8 开始支持默认方法和静态方法。
JDK 8 默认方法 Demo:
interface PayService {
void pay(int amount);
default void validate(int amount) {
if (amount <= 0) {
throw new IllegalArgumentException("金额必须大于 0");
}
}
}
class AliPayService implements PayService {
public void pay(int amount) {
validate(amount);
System.out.println("支付宝支付:" + amount);
}
}为什么要加默认方法:如果一个接口已经被很多类实现,直接新增抽象方法会导致所有实现类编译失败。默认方法可以给接口增加能力,同时保持旧实现兼容。
日期时间:Date 和 java.time 区别
JDK 8 之前常用 Date、Calendar、SimpleDateFormat,但它们有几个问题:
Date可变,容易被意外修改。CalendarAPI 繁琐。SimpleDateFormat线程不安全。- 本地时间、时间戳、时区表达不清楚。
JDK 8 推荐:
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
public class Jdk8TimeDemo {
public static void main(String[] args) {
LocalDateTime localTime = LocalDateTime.of(2026, 7, 2, 10, 30);
ZonedDateTime shanghaiTime = localTime.atZone(ZoneId.of("Asia/Shanghai"));
String text = shanghaiTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"));
System.out.println(text);
}
}商业项目里,订单创建时间、支付时间、账单周期、定时任务触发时间都必须明确时区策略。如果不会区分 LocalDateTime、Instant、ZonedDateTime,跨地区系统很容易出现时间错乱。
后续版本常见差异
| 版本 | 重要变化 | 学习重点 |
|---|---|---|
| JDK 9 | 模块化 JPMS、集合工厂方法、接口私有方法 | 模块边界、反射访问限制 |
| JDK 10 | var 局部变量类型推断 | 只适合局部变量,不要牺牲可读性 |
| JDK 11 | 标准 HTTP Client、字符串增强、LTS | 现代服务调用、长期支持版本 |
| JDK 14/16 | switch 表达式、record | 减少样板代码 |
| JDK 17 | sealed class、模式匹配基础、LTS | 现代 Java 生产升级常见目标 |
| JDK 21 | 虚拟线程、模式匹配增强、LTS | 高并发 IO 场景 |
| JDK 25 | LTS 演进版本 | 跟踪新版本特性和兼容性 |
老项目不能用新特性怎么办
| 需求 | JDK 7 写法 | JDK 8 写法 | 新版本写法 |
|---|---|---|---|
| 集合过滤 | for 循环 | Stream | Stream 继续可用 |
| 异步任务 | ExecutorService + Future | CompletableFuture | CompletableFuture + 虚拟线程场景 |
| 日期时间 | Date/Calendar | java.time | java.time 继续可用 |
| 数据载体 | 普通 JavaBean | JavaBean / Lombok | record |
| 类型判断 | instanceof + 强转 | instanceof + 强转 | 模式匹配 |
| HTTP 调用 | HttpURLConnection / Apache HttpClient | 第三方库常见 | Java 11 HttpClient |
学习时要先掌握旧写法,因为你很可能维护旧项目;再掌握新写法,因为新项目和框架会越来越多使用现代 Java。
JDK 8 Lambda 和函数式接口原理
Lambda 不是“语法糖这么简单”。它背后依赖函数式接口和 invokedynamic。
函数式接口是只有一个抽象方法的接口。例如:
@FunctionalInterface
interface OrderFilter {
boolean accept(Order order);
}JDK 7 写策略常用匿名内部类:
List<Order> filter(List<Order> orders, OrderFilter filter) {
List<Order> result = new ArrayList<Order>();
for (Order order : orders) {
if (filter.accept(order)) {
result.add(order);
}
}
return result;
}
List<Order> paidOrders = filter(orders, new OrderFilter() {
public boolean accept(Order order) {
return "PAID".equals(order.getStatus());
}
});JDK 8 可以写成:
List<Order> paidOrders = filter(orders, order -> "PAID".equals(order.getStatus()));它解决的是“把行为作为参数传递”的问题。排序、过滤、回调、策略、异步任务都可以更简洁。
执行链路可以这样理解:
flowchart TD
A["Lambda 表达式"] --> B["目标类型:函数式接口"]
B --> C["编译器检查唯一抽象方法签名"]
C --> D["生成 invokedynamic 调用点"]
D --> E["运行时通过 LambdaMetafactory 创建函数对象"]
E --> F["调用函数式接口方法"]为什么 JDK 8 不直接把 Lambda 编译成匿名内部类?匿名内部类会生成额外 class 文件,捕获变量和加载也更重。invokedynamic 可以把“如何创建 Lambda 对象”的策略推迟到运行时,由 JVM 更灵活地优化。
Lambda 捕获局部变量时要求变量是 final 或“事实 final”:
int base = 10;
Runnable task = () -> System.out.println(base);不能在后面修改 base。因为局部变量在线程栈里,Lambda 对象可能比方法栈帧活得更久。Java 捕获的是变量值而不是可变栈槽,要求事实 final 能避免语义混乱。
不理解 Lambda 的后果:
- 把复杂业务都塞进 Lambda,导致可读性下降。
- 在 Stream 里做数据库调用、远程调用,性能和事务边界混乱。
- 误用 parallelStream,把阻塞 IO 放进公共 ForkJoinPool。
- 不理解变量捕获,写出不符合预期的异步代码。
Stream 原理和常见坑
Stream 是声明式数据处理管道,不是集合本身。它不会存储数据,而是描述“数据从哪里来、经过哪些中间操作、最终怎么收集”。
flowchart TD
A["数据源 List"] --> B["创建 Stream"]
B --> C["中间操作 filter/map/sorted"]
C --> D["终止操作 collect/count/forEach"]
D --> E["真正遍历执行"]Stream 的重要特点:
| 特点 | 说明 |
|---|---|
| 惰性执行 | 没有终止操作,中间操作不会真正执行 |
| 单次使用 | 一个 Stream 只能消费一次 |
| 不改变原集合 | 通常返回新结果,除非你在 Lambda 里修改外部对象 |
| 可读性优先 | 适合过滤、映射、分组、统计 |
惰性执行 Demo:
import java.util.Arrays;
import java.util.List;
public class StreamLazyDemo {
public static void main(String[] args) {
List<String> names = Arrays.asList("tom", "jerry", "tony");
names.stream()
.filter(name -> {
System.out.println("filter: " + name);
return name.startsWith("t");
});
System.out.println("没有终止操作,上面的 filter 不会执行");
}
}加上终止操作后才执行:
long count = names.stream()
.filter(name -> {
System.out.println("filter: " + name);
return name.startsWith("t");
})
.count();商业项目常见坑:
| 坑 | 后果 | 正确做法 |
|---|---|---|
| Stream 里查数据库 | N+1 查询、事务边界混乱 | 先批量查,再内存映射 |
| parallelStream 调阻塞接口 | 公共线程池被占满,影响其他并行任务 | 使用自定义线程池或 CompletableFuture |
| 在 map/filter 里修改外部集合 | 并发和可读性问题 | 保持无副作用,最后 collect |
| 对大集合无限制 collect | 堆内存上涨,可能 OOM | 分页、分批、流式处理 |
Optional:解决什么,不解决什么
Optional 的目标是让“可能没有值”在方法返回值上显式表达,减少调用方忘记判空。
推荐用法:
import java.util.Optional;
class UserRepository {
Optional<User> findById(Long id) {
User user = queryFromDb(id);
return Optional.ofNullable(user);
}
}
User user = repository.findById(1001L)
.orElseThrow(() -> new IllegalArgumentException("用户不存在"));不推荐用法:
class User {
private Optional<String> name; // 不推荐作为实体字段
}为什么不推荐把 Optional 放字段里?它主要是返回值语义,不是序列化字段类型。实体、DTO、JSON 序列化中大量使用 Optional 字段会增加框架适配复杂度。
Optional 也不能消灭所有空指针:
- 你仍然可能传入
null的 Optional 变量。 Optional.get()不判断就调用仍然可能异常。- 链路上的集合、字段、外部接口仍然可能为空。
正确理解:Optional 是提醒调用者处理“可能为空”,不是让空值问题自动消失。
CompletableFuture:JDK 8 异步编排为什么重要
JDK 7 的 Future 只能提交任务后 get() 等结果,不擅长组合任务。JDK 8 的 CompletableFuture 可以组织异步依赖、并行聚合和异常兜底。
业务场景:订单详情页要查订单、库存、优惠券、物流。串行查询会很慢,可以并行聚合:
CompletableFuture<Order> orderFuture =
CompletableFuture.supplyAsync(() -> orderService.getOrder(orderId), bizPool);
CompletableFuture<Stock> stockFuture =
CompletableFuture.supplyAsync(() -> stockService.getStock(orderId), bizPool);
CompletableFuture<Coupon> couponFuture =
CompletableFuture.supplyAsync(() -> couponService.getCoupon(orderId), bizPool);
CompletableFuture<OrderView> viewFuture = orderFuture.thenCombine(stockFuture, (order, stock) -> {
OrderView view = new OrderView();
view.setOrder(order);
view.setStock(stock);
return view;
}).thenCombine(couponFuture, (view, coupon) -> {
view.setCoupon(coupon);
return view;
});
OrderView view = viewFuture.get();执行流程:
flowchart TD
A["提交订单查询"] --> D["等待合并"]
B["提交库存查询"] --> D
C["提交优惠券查询"] --> E["第二次合并"]
D --> E
E --> F["生成聚合结果"]生产注意:
- 必须指定业务线程池,不要默认依赖公共 ForkJoinPool。
- 每个下游调用要有超时。
- 失败要有兜底,不是所有依赖失败都应该让主接口失败。
- 不要在任务提交后立刻
get(),否则并行退化成串行。 - 线程池大小要结合下游连接池和限流能力。
JDK 7/8 JVM 和运行差异
版本差异不只在语法,也在 JVM 默认行为上。
| 点 | JDK 7 | JDK 8 |
|---|---|---|
| 永久代/元空间 | 主要使用 PermGen | 移除 PermGen,使用 Metaspace |
| 类元数据位置 | JVM 管理的永久代 | 本地内存中的元空间 |
| 字符串常量池 | JDK 7 已移动到堆 | 继续在堆中 |
| Lambda 支持 | 不支持 | 支持 invokedynamic Lambda |
| 默认 GC 组合 | 依发行版和参数 | 常见 Parallel/CMS/G1 可选 |
JDK 8 移除永久代后,很多人以为不会再出现类元数据 OOM,这是误解。元空间使用本地内存,如果动态生成类太多、类加载器泄漏、反复热部署不释放,仍然可能 OutOfMemoryError: Metaspace。
排查思路:
flowchart TD
A["Metaspace OOM"] --> B["查看是否动态生成大量类"]
B --> C["检查 CGLIB、Javassist、代理类"]
C --> D["检查类加载器是否泄漏"]
D --> E["查看 jcmd VM.classloader_stats"]
E --> F["修复类加载器引用或限制动态生成"]JDK 11、17、21 学到什么程度
这一节用于快速判断学习深度。四个LTS版本的完整特性、可运行代码、迁移步骤和面试追问见JDK 8、11、17、21 LTS版本演进。
用户强调不能只基于 JDK 21,但后续 LTS 也要知道。建议学习优先级:
| 版本 | 学习深度 | 原因 |
|---|---|---|
| JDK 7 | 会读会维护 | 老项目、传统写法、资源关闭、NIO.2 |
| JDK 8 | 必须深入 | 企业主流、面试高频、Lambda/Stream/CHM/HashMap |
| JDK 11 | 知道生产升级点 | LTS、HTTP Client、字符串和运行工具增强 |
| JDK 17 | 新项目常见目标 | LTS、sealed、record、模式匹配基础 |
| JDK 21 | 理解虚拟线程边界 | LTS、高并发 IO 新方案,但不是所有项目都能用 |
虚拟线程不是线程池的万能替代。它适合大量阻塞 IO,但不能让 CPU 密集任务变快,也不能绕过数据库连接池、下游限流、事务锁和业务容量。
版本升级关注点
从 JDK 8 升级到 11/17/21,不能只改编译版本。要检查:
- 第三方依赖是否支持目标 JDK。
- 反射访问是否被模块强封装影响。
- GC 参数是否废弃或语义变化。
- 字符集、TLS、加密算法默认值是否变化。
- 构建工具 Maven/Gradle 插件是否兼容。
- 容器内存识别和 JVM 参数是否合理。
- 线上评估和回滚方案是否准备好。
升级流程:
flowchart TD
A["盘点依赖和运行参数"] --> B["本地编译和单元测试"]
B --> C["修复废弃 API 和反射访问"]
C --> D["测试环境全量回归"]
D --> E["压测和 GC 对比"]
E --> F["灰度发布"]
F --> G{"指标是否正常"}
G -- "正常" --> H["扩大流量"]
G -- "异常" --> I["回滚旧 JDK"]面试标准回答
问题:JDK 7 和 JDK 8 有哪些重要区别?
标准回答:
JDK 8 相比 JDK 7 最大变化是引入了 Lambda、Stream、函数式接口、Optional、java.time、CompletableFuture,以及接口默认方法和静态方法。集合方面,HashMap 从数组加链表变成数组加链表加红黑树,ConcurrentHashMap 从 Segment 分段锁变成 CAS 加 synchronized 的桶级并发控制。JDK 7 也有重要改进,比如 try-with-resources、switch 支持 String、菱形语法和 NIO.2。实际项目里,JDK 8 是企业主流分水岭,JDK 7 更偏传统写法,后续 JDK 11、17、21 则更多是工程化、语法简化和并发能力增强。
追问:JDK 8 的 Stream 一定比 for 循环快吗?
不是。Stream 主要提升表达能力,不保证一定更快。小数据量下普通 for 循环可能更直接;Stream 适合表达过滤、映射、分组等数据处理流程。parallelStream 还会使用公共 ForkJoinPool,如果用于阻塞 IO,可能拖慢其他任务。
关联知识点
- JDK 8、11、17、21 LTS版本演进
- JDK 8新增特性与实战
- JDK 11新增特性与实战
- JDK 17新增特性与实战
- JDK 21新增特性与实战
- 集合框架
- Lambda与Stream
- 日期时间API
- CompletableFuture
- 线程池总览
- JVM 学习路线
本章小结
Java 版本差异要按演进理解。JDK 7 是传统 Java 写法的重要基础,JDK 8 是企业主流和面试高频的分水岭,JDK 11/17/21/25 是后续长期支持和现代能力演进。学习时不能只看新版本,也不能停留在老写法;要知道旧版本怎么做,新版本为什么改,项目里如何兼容。
