前言

Java 基础面试题看起来范围很大:有的问 equalshashCode,有的问 String、泛型、异常、集合、线程池、锁、JVM、类加载,也有的会继续追问 JDK 8、JDK 17、JDK 21 分别带来了哪些重要变化。

如果只背零散结论,遇到追问很容易断。更好的方式是把 Java 基础理解成几条主线:

1
语言基础 -> 面向对象 -> 标准类库 -> 集合与泛型 -> 并发与内存模型 -> JVM 与类加载 -> JDK 版本演进

本文按面试中最常见的模块梳理 Java 基础要点,并专门总结 JDK 8、17、21 的新特性,适合作为面试前的复习清单。

Java 整体特点

Java 常见特点包括:

  • 面向对象。
  • 跨平台,依赖 JVM。
  • 自动内存管理和垃圾回收。
  • 静态类型语言。
  • 标准库和生态成熟。
  • 支持多线程和并发编程。

面试中可以这样概括:

Java 源码编译成字节码,字节码运行在 JVM 上。JVM 屏蔽不同操作系统差异,并通过类加载、运行时数据区、JIT 编译和垃圾回收支撑程序运行。

flowchart TD
    A[Java Source] --> B[javac 编译]
    B --> C[Bytecode .class]
    C --> D[JVM]
    D --> E[ClassLoader]
    D --> F[Runtime Data Area]
    D --> G[JIT Compiler]
    D --> H[GC]
    D --> I[Operating System]

基础语法

Java 基础语法面试常问数据类型、包装类型、自动装箱、字符串和运算规则。

基本类型

Java 有 8 种基本类型:

类型 说明
byte 1 字节整数
short 2 字节整数
int 4 字节整数
long 8 字节整数
float 单精度浮点数
double 双精度浮点数
char 字符
boolean 布尔值

基本类型不是对象,存储和计算成本较低。包装类型如 IntegerLong 是对象,可以用于泛型、集合和空值表达。

自动装箱和拆箱

自动装箱是基本类型转包装类型,拆箱是包装类型转基本类型。

1
2
Integer a = 10; // 装箱
int b = a; // 拆箱

常见坑:

1
2
3
Integer a = 128;
Integer b = 128;
System.out.println(a == b); // false

Integer 默认缓存 -128127,超过范围后可能创建新对象。包装类型比较数值应使用 equals,但要注意空指针。

String

String 是不可变对象。字符串内容一旦创建不能修改,拼接、替换等操作会产生新对象。

不可变的好处:

  • 线程安全。
  • 可以缓存 hash。
  • 适合作为 Map key。
  • 字符串常量池可以复用。
  • 安全性更好,避免引用被外部修改。

常见对比:

  • String:不可变,适合少量字符串操作。
  • StringBuilder:可变,非线程安全,性能较好。
  • StringBuffer:可变,方法同步,线程安全但开销更高。

面试常问:

1
2
3
String a = "hello";
String b = "hello";
String c = new String("hello");

a == b 通常为 true,因为都指向字符串常量池;a == c 为 false,因为 new 创建了堆对象。比较内容要用 equals

面向对象

Java 面向对象核心包括封装、继承、多态。

  • 封装:隐藏内部实现,通过方法暴露能力。
  • 继承:子类复用父类能力。
  • 多态:父类引用指向子类对象,运行时根据真实类型调用方法。

常见问题:

重载和重写

重载发生在同一个类中,方法名相同,参数列表不同,编译期决定。

重写发生在父子类之间,子类覆盖父类方法,运行期根据对象真实类型决定。

抽象类和接口

抽象类适合表达“是什么”,可以有状态和部分实现。

接口适合表达“能做什么”,强调能力和规范。

JDK 8 后接口可以有默认方法和静态方法,JDK 9 后接口可以有私有方法,用于复用接口内部逻辑。

equals 和 hashCode

equals 用于判断对象逻辑相等,hashCode 用于哈希容器定位桶位置。

约定:

  • 两个对象 equals 为 true,hashCode 必须相等。
  • 两个对象 hashCode 相等,equals 不一定为 true。
  • 重写 equals 通常必须重写 hashCode

如果只重写 equals 不重写 hashCode,对象放入 HashMapHashSet 时可能出现查不到、重复存储等问题。

异常

Java 异常体系:

1
2
3
4
5
Throwable
├── Error
└── Exception
├── RuntimeException
└── Checked Exception
  • Error:严重错误,通常不应捕获,例如 OutOfMemoryError
  • Checked Exception:编译期要求处理,例如 IOException
  • RuntimeException:运行时异常,例如 NullPointerExceptionIllegalArgumentException

常见实践:

  • 不要吞异常。
  • 不要直接捕获 Exception 后只打印。
  • 业务异常要有明确错误码。
  • 资源关闭优先使用 try-with-resources。
  • 日志中保留堆栈和上下文。

泛型

泛型用于在编译期提供类型约束,减少强制类型转换。

1
List<String> names = new ArrayList<>();

Java 泛型使用类型擦除实现。运行时大多数泛型信息会被擦除为原始类型。

常见问题:

  • 不能直接创建泛型数组。
  • List<String>List<Integer> 运行时类型都是 List
  • 泛型主要保证编译期类型安全。

通配符:

  • ? extends T:上界通配符,适合读。
  • ? super T:下界通配符,适合写。

常见口诀是 PECS:Producer Extends, Consumer Super。

集合框架

Java 集合核心包括 List、Set、Map、Queue。

List

  • ArrayList:底层数组,查询快,尾部追加快,中间插入删除可能慢。
  • LinkedList:底层链表,插入删除有优势,但随机访问慢。

实际项目中 ArrayList 更常用,因为 CPU 缓存友好,遍历和随机访问性能更好。

Set

  • HashSet:基于 HashMap,元素不重复,无序。
  • LinkedHashSet:保持插入顺序。
  • TreeSet:基于红黑树,支持排序。

Map

  • HashMap:数组 + 链表 + 红黑树,非线程安全。
  • LinkedHashMap:保持插入顺序或访问顺序,适合 LRU。
  • TreeMap:基于红黑树,按 key 排序。
  • ConcurrentHashMap:线程安全,高并发场景常用。

HashMap

HashMap 高频问题包括底层结构、扩容、哈希冲突和线程安全。

JDK 8 中 HashMap 结构:

1
数组 + 链表 + 红黑树

当链表长度超过阈值并且数组容量足够时,链表会转为红黑树,降低极端哈希冲突下的查询成本。

常见参数:

  • 初始容量默认 16。
  • 负载因子默认 0.75。
  • 超过阈值触发扩容。

HashMap 不是线程安全的。多线程写入可能导致数据错乱。并发场景应使用 ConcurrentHashMap。

ConcurrentHashMap

JDK 8 的 ConcurrentHashMap 使用 CAS + synchronized + volatile 等机制提升并发能力。

特点:

  • 读操作大多无锁。
  • 写操作锁粒度较小。
  • 通过 volatile 保证可见性。
  • 扩容时支持多线程协助迁移。

它适合高并发 Map 场景,但不代表所有复合操作都是原子的。例如先 getput 仍可能有并发问题,可以使用 computeIfAbsent 等原子方法。

并发基础

Java 并发面试常问线程创建、线程池、锁、volatile、synchronized、AQS、ThreadLocal。

线程创建方式

常见方式:

  • 继承 Thread。
  • 实现 Runnable。
  • 实现 Callable 配合 FutureTask。
  • 使用线程池。

生产环境更推荐线程池,而不是频繁创建线程。

synchronized

synchronized 是 JVM 层面的内置锁,可以修饰方法或代码块。

它保证:

  • 原子性。
  • 可见性。
  • 有序性的一部分约束。

锁对象不同,保护范围也不同:

  • 实例方法锁当前对象。
  • 静态方法锁 Class 对象。
  • 代码块锁指定对象。

volatile

volatile 保证可见性和禁止指令重排序,但不保证复合操作原子性。

例如:

1
2
volatile int count = 0;
count++;

count++ 不是原子操作,仍然有并发问题。

volatile 常用于状态标志、双重检查锁定中的实例引用等场景。

线程池

线程池核心参数:

  • corePoolSize:核心线程数。
  • maximumPoolSize:最大线程数。
  • keepAliveTime:非核心线程空闲存活时间。
  • workQueue:任务队列。
  • threadFactory:线程工厂。
  • rejectedExecutionHandler:拒绝策略。

执行流程:

1
提交任务 -> 核心线程未满则创建 -> 否则入队 -> 队列满则创建非核心线程 -> 达到最大线程数后触发拒绝策略

常见拒绝策略:

  • AbortPolicy:抛异常。
  • CallerRunsPolicy:调用者线程执行。
  • DiscardPolicy:直接丢弃。
  • DiscardOldestPolicy:丢弃队列最旧任务。

面试中要注意:不建议直接使用 Executors.newFixedThreadPool 等快捷方法,因为队列可能无界,容易导致内存风险。更推荐显式使用 ThreadPoolExecutor

ThreadLocal

ThreadLocal 为每个线程提供独立变量副本。

常见用途:

  • 保存用户上下文。
  • 保存 traceId。
  • 保存事务或连接上下文。
  • 保存日期格式化对象。

常见风险:

  • 线程池中线程会复用。
  • 使用后不 remove 可能导致上下文污染。
  • ThreadLocalMap 的 key 是弱引用,value 仍可能残留。

最佳实践:使用完后在 finally 中清理。

Java 内存模型

Java 内存模型关注多线程下共享变量的可见性、有序性和原子性。

核心概念:

  • 主内存。
  • 工作内存。
  • happens-before。
  • volatile。
  • synchronized。
  • final 语义。

happens-before 常见规则:

  • 程序顺序规则。
  • volatile 写先行发生于后续 volatile 读。
  • 解锁先行发生于后续加锁。
  • 线程 start 先行发生于线程内动作。
  • 线程内所有操作先行发生于其他线程检测到该线程结束。

JVM 内存区域

运行时数据区常见划分:

  • 程序计数器。
  • Java 虚拟机栈。
  • 本地方法栈。
  • 堆。
  • 方法区。
  • 运行时常量池。

线程私有:

  • 程序计数器。
  • 虚拟机栈。
  • 本地方法栈。

线程共享:

  • 堆。
  • 方法区。

常见异常:

  • StackOverflowError:栈深度过深。
  • OutOfMemoryError: Java heap space:堆内存不足。
  • OutOfMemoryError: Metaspace:元空间不足。

类加载

类加载流程:

1
加载 -> 验证 -> 准备 -> 解析 -> 初始化

类加载器:

  • Bootstrap ClassLoader。
  • Platform ClassLoader。
  • Application ClassLoader。
  • 自定义 ClassLoader。

双亲委派模型的作用:

  • 避免核心类被重复加载。
  • 防止核心 API 被篡改。
  • 保证类加载层次稳定。

常见追问:什么时候会打破双亲委派?

例如 SPI、应用服务器隔离、热部署、OSGi、某些插件化场景。

垃圾回收

GC 面试常问对象是否可回收、垃圾收集器、GC 日志和调优。

判断对象是否存活:

  • 引用计数法。
  • 可达性分析。

Java 主流使用可达性分析,从 GC Roots 出发,无法到达的对象可回收。

常见 GC Roots:

  • 虚拟机栈中引用的对象。
  • 方法区静态属性引用的对象。
  • 常量引用的对象。
  • 本地方法栈 JNI 引用的对象。

常见收集器:

  • Serial。
  • Parallel。
  • CMS。
  • G1。
  • ZGC。

JDK 17、21 中 G1 和 ZGC 都是面试中常见话题。G1 面向可预测停顿,ZGC 目标是低延迟。

JDK 8 新特性

JDK 8 是 Java 生态影响最大的版本之一,面试高频。

Lambda 表达式

Lambda 让函数式接口写法更简洁。

1
2
List<String> names = List.of("Tom", "Jerry");
names.forEach(name -> System.out.println(name));

它适合简化回调、集合遍历、函数式接口实现。

函数式接口

函数式接口只有一个抽象方法,可以使用 @FunctionalInterface 标记。

常见接口:

  • Function。
  • Consumer。
  • Supplier。
  • Predicate。

Stream API

Stream 用于集合流水线处理。

1
2
3
4
List<String> result = names.stream()
.filter(name -> name.length() > 3)
.map(String::toUpperCase)
.toList();

常见操作:

  • filter。
  • map。
  • flatMap。
  • sorted。
  • distinct。
  • collect。
  • reduce。

注意:Stream 不是数据库查询引擎。大量数据、复杂过滤仍应在数据库层完成。

Optional

Optional 用于表达可能为空的返回值,减少直接空指针。

1
Optional<User> user = userRepository.findById(id);

不要滥用 Optional 作为实体字段,也不要为了 Optional 而写更复杂的代码。

默认方法

接口可以定义 default 方法,便于接口演进。

1
2
3
default void log() {
System.out.println("default log");
}

新日期时间 API

JDK 8 引入 java.time

  • LocalDate。
  • LocalTime。
  • LocalDateTime。
  • ZonedDateTime。
  • Instant。
  • Duration。
  • Period。

相比 Date 和 Calendar,新 API 不可变、线程安全、语义清晰。

CompletableFuture

CompletableFuture 用于异步编排。

1
2
3
CompletableFuture.supplyAsync(() -> queryUser())
.thenApply(user -> buildDTO(user))
.thenAccept(dto -> save(dto));

使用时要注意线程池,不要把阻塞任务随意丢到公共 ForkJoinPool。

Metaspace

JDK 8 移除了永久代,改用 Metaspace。类元数据主要存放在本地内存中。

常见问题是类加载过多、动态代理过多、热部署泄漏导致 Metaspace OOM。

JDK 17 新特性

JDK 17 是长期支持版本,从 JDK 8 升级时很常见。

var 局部变量类型推断

var 是 JDK 10 引入的局部变量类型推断,JDK 17 中已稳定可用。

1
var name = "Java";

它只能用于局部变量,不能用于成员变量、方法参数、返回值。使用时要保证代码可读性。

文本块

文本块适合多行字符串。

1
2
3
4
5
String sql = """
select *
from user
where status = ?
""";

适合 SQL、JSON、HTML 等多行文本。

switch 表达式

switch 可以作为表达式返回值。

1
2
3
4
5
String type = switch (status) {
case 1 -> "启用";
case 0 -> "禁用";
default -> "未知";
};

写法更简洁,也减少 break 遗漏问题。

record

record 用于声明不可变数据载体。

1
public record UserDTO(Long id, String name) {}

编译器自动生成构造方法、访问器、equals、hashCode、toString。

适合 DTO、配置项、返回值对象,不适合复杂可变领域对象。

sealed class

密封类限制哪些类可以继承。

1
public sealed class Result permits Success, Failure {}

适合表达有限类型层次,配合模式匹配可以让分支处理更安全。

instanceof 模式匹配

1
2
3
if (obj instanceof String s) {
System.out.println(s.length());
}

减少强制类型转换样板代码。

Helpful NullPointerException

JDK 14 引入更明确的空指针提示,JDK 17 中可用。异常信息能指出哪一段表达式为 null,提升排查效率。

ZGC 和 G1 改进

JDK 17 中 G1 是默认垃圾收集器,ZGC 已经成熟可用。对低延迟服务,ZGC 是重要选择之一,但是否使用仍要结合吞吐、延迟、内存和运维经验压测。

JDK 21 新特性

JDK 21 也是长期支持版本,核心亮点集中在虚拟线程、模式匹配和现代化并发模型。

虚拟线程

虚拟线程是 JDK 21 最重要的新特性之一。

1
2
3
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> callRemoteService());
}

特点:

  • 创建成本远低于平台线程。
  • 适合大量阻塞 IO 任务。
  • 能简化传统异步回调模型。
  • 不适合直接提升 CPU 密集型任务性能。

面试中要强调:虚拟线程不是让 CPU 变多,而是降低阻塞 IO 场景下线程资源成本。

结构化并发

结构化并发在 JDK 21 中是预览特性,用于把一组并发任务视为一个整体管理,便于取消、异常传播和结果汇总。

适合一个请求内并行调用多个下游服务的场景。

由于是预览特性,生产使用要看团队规范和版本策略。

Sequenced Collections

JDK 21 引入有序集合接口,统一表达集合的首尾访问能力。

相关接口:

  • SequencedCollection。
  • SequencedSet。
  • SequencedMap。

可以更统一地访问第一个、最后一个元素以及反向视图。

switch 模式匹配

JDK 21 中 switch 模式匹配正式可用。

1
2
3
4
5
6
String text = switch (obj) {
case String s -> s;
case Integer i -> "number: " + i;
case null -> "null";
default -> "unknown";
};

它让类型判断和分支处理更简洁,配合 sealed class 更适合表达有限状态和结果类型。

record patterns

record patterns 可用于解构 record。

1
2
3
4
5
record Point(int x, int y) {}

if (obj instanceof Point(int x, int y)) {
System.out.println(x + "," + y);
}

适合处理不可变数据载体和模式匹配。

字符串模板

字符串模板在 JDK 21 中是预览特性,用于更安全、可读地表达字符串插值。因为仍是预览特性,面试中知道即可,生产使用要谨慎。

JDK 8、17、21 对比

版本 定位 高频特性
JDK 8 经典长期支持版本,生态基础 Lambda、Stream、Optional、java.time、CompletableFuture、接口默认方法、Metaspace
JDK 17 现代长期支持版本,很多语法改进稳定 record、sealed class、switch 表达式、文本块、instanceof 模式匹配、G1/ZGC 改进
JDK 21 新一代长期支持版本,并发模型升级明显 虚拟线程、Sequenced Collections、switch 模式匹配、record patterns、结构化并发预览

升级时要关注:

  • 依赖库兼容。
  • 构建工具版本。
  • 框架版本,例如 Spring Boot 对 JDK 的支持矩阵。
  • JVM 参数变化。
  • GC 策略变化。
  • 反射和模块访问限制。
  • 容器镜像和运行时基线。

常见排查思路

如果面试官问“Java 应用 CPU 高怎么排查”,可以按以下路径回答:

  1. 用 top 找到高 CPU 进程。
  2. 找到高 CPU 线程。
  3. 转换线程 ID 为十六进制。
  4. 用 jstack 查看线程栈。
  5. 定位是死循环、频繁 GC、锁竞争还是高并发计算。
  6. 结合业务日志和最近发布判断原因。

如果内存高或 OOM:

  1. 看堆内存、元空间、直接内存还是线程数。
  2. 查看 GC 日志。
  3. dump heap 分析大对象和引用链。
  4. 检查缓存、集合、ThreadLocal、连接池、动态代理。
  5. 区分内存泄漏和正常流量增长。

如果接口慢:

  1. 看线程池是否耗尽。
  2. 看数据库和远程调用耗时。
  3. 看锁等待。
  4. 看 GC 暂停。
  5. 看日志和 traceId。

高频面试题

Java 为什么能跨平台?

Java 源码编译成字节码,字节码运行在不同平台的 JVM 上。JVM 屏蔽操作系统差异,实现“一次编译,到处运行”。

== 和 equals 有什么区别?

== 比较基本类型值,比较引用类型地址;equals 默认也是比较地址,但很多类会重写为逻辑相等,例如 String。

HashMap 为什么线程不安全?

HashMap 没有同步控制,多线程同时 put、resize 或修改链表/树结构时可能出现数据覆盖、结构异常或读取不一致。并发场景应使用 ConcurrentHashMap。

volatile 能替代锁吗?

不能完全替代。volatile 保证可见性和禁止重排序,但不保证复合操作原子性。计数、余额修改这类操作仍需要锁或原子类。

JDK 8 最重要的新特性是什么?

Lambda、函数式接口、Stream API、Optional、java.time、新的 CompletableFuture、接口默认方法,以及永久代改为 Metaspace。

JDK 17 有哪些重要特性?

record、sealed class、switch 表达式、文本块、instanceof 模式匹配、Helpful NullPointerException,以及 G1/ZGC 等 JVM 能力改进。

JDK 21 最值得关注的特性是什么?

虚拟线程是最值得关注的特性,适合大量阻塞 IO 场景;此外还有 Sequenced Collections、switch 模式匹配、record patterns、结构化并发预览等。

总结

Java 基础面试的主线可以围绕四个问题展开:

  1. 语言怎么表达:基础类型、面向对象、异常、泛型、字符串。
  2. 类库怎么使用:集合、Map、Stream、Optional、时间 API。
  3. 并发怎么保证:线程池、锁、volatile、ThreadLocal、JMM。
  4. 运行时怎么工作:JVM、类加载、GC、JDK 8/17/21 版本演进。

把这条链路讲清楚,再结合 HashMap、ConcurrentHashMap、线程池、JVM 排查和 JDK 新特性,Java 基础相关问题就能从“背 API”升级成“理解语言和运行时机制”。