Skip to content

JDK 7、JDK 8 与后续版本差异

Java 学习不能只看最新版本。大量企业项目仍运行在 JDK 8,很多老系统甚至还在 JDK 7。面试也经常追问“JDK 7 和 JDK 8 有什么区别”“HashMap 在 JDK 7 和 JDK 8 有什么变化”“为什么 JDK 8 引入 Lambda”。所以学习 JavaSE 要按版本演进理解,而不是只背 API。

学习目标

学完本章要能回答:

  1. JDK 7、JDK 8、JDK 11、JDK 17、JDK 21 常见差异是什么。
  2. 为什么 JDK 8 是企业 Java 的关键分水岭。
  3. JDK 7 和 JDK 8 在集合、并发、接口、日期时间、函数式编程上有什么区别。
  4. 新版本特性在老项目中不能用时,应该如何替代。
  5. 面试被问版本差异时,如何从“语言、集合、并发、JVM、工程化”几个角度回答。

版本演进主线

mermaid
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 做 switchswitch (type)
try-with-resources自动关闭资源,减少 finally 样板代码关闭文件流、连接
菱形语法泛型实例化少写重复类型new ArrayList<>()
多异常捕获一个 catch 捕获多个异常`catch (IOException
NIO.2更好的文件 APIPathFiles

JDK 7 写法 Demo

java
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 常见写法:

java
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 写法:

java
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 区别

这是面试高频点。

mermaid
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 7JDK 8
数据结构数组 + 链表数组 + 链表 + 红黑树
插入方式头插法尾插法
扩容迁移多线程下可能形成环优化迁移逻辑,但仍非线程安全
极端冲突链表过长退化为 O(n)树化后接近 O(log n)
并发安全不安全仍然不安全

为什么 JDK 8 加红黑树:如果大量 key 哈希冲突,链表查询会从接近 O(1) 退化为 O(n)。红黑树能降低极端情况下的查询成本。但这不代表业务可以忽略 hashCode 质量,也不代表 HashMap 可以并发写。

ConcurrentHashMap:JDK 7 和 JDK 8 区别

mermaid
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 7JDK 8
锁粒度Segment 级别桶级别
底层结构Segment + HashEntryNode 数组 + 链表 + 红黑树
写空桶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:

java
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 之前常用 DateCalendarSimpleDateFormat,但它们有几个问题:

  1. Date 可变,容易被意外修改。
  2. Calendar API 繁琐。
  3. SimpleDateFormat 线程不安全。
  4. 本地时间、时间戳、时区表达不清楚。

JDK 8 推荐:

java
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);
    }
}

商业项目里,订单创建时间、支付时间、账单周期、定时任务触发时间都必须明确时区策略。如果不会区分 LocalDateTimeInstantZonedDateTime,跨地区系统很容易出现时间错乱。

后续版本常见差异

版本重要变化学习重点
JDK 9模块化 JPMS、集合工厂方法、接口私有方法模块边界、反射访问限制
JDK 10var 局部变量类型推断只适合局部变量,不要牺牲可读性
JDK 11标准 HTTP Client、字符串增强、LTS现代服务调用、长期支持版本
JDK 14/16switch 表达式、record减少样板代码
JDK 17sealed class、模式匹配基础、LTS现代 Java 生产升级常见目标
JDK 21虚拟线程、模式匹配增强、LTS高并发 IO 场景
JDK 25LTS 演进版本跟踪新版本特性和兼容性

老项目不能用新特性怎么办

需求JDK 7 写法JDK 8 写法新版本写法
集合过滤for 循环StreamStream 继续可用
异步任务ExecutorService + FutureCompletableFutureCompletableFuture + 虚拟线程场景
日期时间Date/Calendarjava.timejava.time 继续可用
数据载体普通 JavaBeanJavaBean / Lombokrecord
类型判断instanceof + 强转instanceof + 强转模式匹配
HTTP 调用HttpURLConnection / Apache HttpClient第三方库常见Java 11 HttpClient

学习时要先掌握旧写法,因为你很可能维护旧项目;再掌握新写法,因为新项目和框架会越来越多使用现代 Java。

JDK 8 Lambda 和函数式接口原理

Lambda 不是“语法糖这么简单”。它背后依赖函数式接口和 invokedynamic

函数式接口是只有一个抽象方法的接口。例如:

java
@FunctionalInterface
interface OrderFilter {
    boolean accept(Order order);
}

JDK 7 写策略常用匿名内部类:

java
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 可以写成:

java
List<Order> paidOrders = filter(orders, order -> "PAID".equals(order.getStatus()));

它解决的是“把行为作为参数传递”的问题。排序、过滤、回调、策略、异步任务都可以更简洁。

执行链路可以这样理解:

mermaid
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”:

java
int base = 10;
Runnable task = () -> System.out.println(base);

不能在后面修改 base。因为局部变量在线程栈里,Lambda 对象可能比方法栈帧活得更久。Java 捕获的是变量值而不是可变栈槽,要求事实 final 能避免语义混乱。

不理解 Lambda 的后果:

  1. 把复杂业务都塞进 Lambda,导致可读性下降。
  2. 在 Stream 里做数据库调用、远程调用,性能和事务边界混乱。
  3. 误用 parallelStream,把阻塞 IO 放进公共 ForkJoinPool。
  4. 不理解变量捕获,写出不符合预期的异步代码。

Stream 原理和常见坑

Stream 是声明式数据处理管道,不是集合本身。它不会存储数据,而是描述“数据从哪里来、经过哪些中间操作、最终怎么收集”。

mermaid
flowchart TD
    A["数据源 List"] --> B["创建 Stream"]
    B --> C["中间操作 filter/map/sorted"]
    C --> D["终止操作 collect/count/forEach"]
    D --> E["真正遍历执行"]

Stream 的重要特点:

特点说明
惰性执行没有终止操作,中间操作不会真正执行
单次使用一个 Stream 只能消费一次
不改变原集合通常返回新结果,除非你在 Lambda 里修改外部对象
可读性优先适合过滤、映射、分组、统计

惰性执行 Demo:

java
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 不会执行");
    }
}

加上终止操作后才执行:

java
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 的目标是让“可能没有值”在方法返回值上显式表达,减少调用方忘记判空。

推荐用法:

java
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("用户不存在"));

不推荐用法:

java
class User {
    private Optional<String> name; // 不推荐作为实体字段
}

为什么不推荐把 Optional 放字段里?它主要是返回值语义,不是序列化字段类型。实体、DTO、JSON 序列化中大量使用 Optional 字段会增加框架适配复杂度。

Optional 也不能消灭所有空指针:

  1. 你仍然可能传入 null 的 Optional 变量。
  2. Optional.get() 不判断就调用仍然可能异常。
  3. 链路上的集合、字段、外部接口仍然可能为空。

正确理解:Optional 是提醒调用者处理“可能为空”,不是让空值问题自动消失。

CompletableFuture:JDK 8 异步编排为什么重要

JDK 7 的 Future 只能提交任务后 get() 等结果,不擅长组合任务。JDK 8 的 CompletableFuture 可以组织异步依赖、并行聚合和异常兜底。

业务场景:订单详情页要查订单、库存、优惠券、物流。串行查询会很慢,可以并行聚合:

java
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();

执行流程:

mermaid
flowchart TD
    A["提交订单查询"] --> D["等待合并"]
    B["提交库存查询"] --> D
    C["提交优惠券查询"] --> E["第二次合并"]
    D --> E
    E --> F["生成聚合结果"]

生产注意:

  1. 必须指定业务线程池,不要默认依赖公共 ForkJoinPool。
  2. 每个下游调用要有超时。
  3. 失败要有兜底,不是所有依赖失败都应该让主接口失败。
  4. 不要在任务提交后立刻 get(),否则并行退化成串行。
  5. 线程池大小要结合下游连接池和限流能力。

JDK 7/8 JVM 和运行差异

版本差异不只在语法,也在 JVM 默认行为上。

JDK 7JDK 8
永久代/元空间主要使用 PermGen移除 PermGen,使用 Metaspace
类元数据位置JVM 管理的永久代本地内存中的元空间
字符串常量池JDK 7 已移动到堆继续在堆中
Lambda 支持不支持支持 invokedynamic Lambda
默认 GC 组合依发行版和参数常见 Parallel/CMS/G1 可选

JDK 8 移除永久代后,很多人以为不会再出现类元数据 OOM,这是误解。元空间使用本地内存,如果动态生成类太多、类加载器泄漏、反复热部署不释放,仍然可能 OutOfMemoryError: Metaspace

排查思路:

mermaid
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,不能只改编译版本。要检查:

  1. 第三方依赖是否支持目标 JDK。
  2. 反射访问是否被模块强封装影响。
  3. GC 参数是否废弃或语义变化。
  4. 字符集、TLS、加密算法默认值是否变化。
  5. 构建工具 Maven/Gradle 插件是否兼容。
  6. 容器内存识别和 JVM 参数是否合理。
  7. 线上评估和回滚方案是否准备好。

升级流程:

mermaid
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,可能拖慢其他任务。

关联知识点

本章小结

Java 版本差异要按演进理解。JDK 7 是传统 Java 写法的重要基础,JDK 8 是企业主流和面试高频的分水岭,JDK 11/17/21/25 是后续长期支持和现代能力演进。学习时不能只看新版本,也不能停留在老写法;要知道旧版本怎么做,新版本为什么改,项目里如何兼容。