1. 基础部分
1.1 Java的基本数据类型有哪些?并简述其占用内存大小和默认值。
Java共有8种基本数据类型,可分为四类:
| 类型 | 关键字 | 占用内存 | 默认值 | 包装类 |
|---|---|---|---|---|
| 字节型 | byte | 1 字节 | 0 | Byte |
| 短整型 | short | 2 字节 | 0 | Short |
| 整型 | int | 4 字节 | 0 | Integer |
| 长整型 | long | 8 字节 | 0L | Long |
| 浮点型 | float | 4 字节 | 0.0f | Float |
| 双精度型 | double | 8 字节 | 0.0d | Double |
| 字符型 | char | 2 字节 | '\u0000'(空字符) | Character |
| 布尔型 | boolean | 1 字节(JVM 中通常用 int 表示) | false | Boolean |
注意事项:
- 基本类型的默认值只在作为类的成员变量时生效;局部变量必须显式初始化,否则编译报错。
boolean的具体内存占用与 JVM 实现有关,规范未严格规定,常见实现中看作int大小。
1.2 请解释Java中的封装、继承和多态概念,并举例说明。
封装(Encapsulation)
将对象的状态(属性)私有化,通过公有的 getter/setter 方法对外暴露访问,实现数据隐藏和安全性控制。
public class Person {
private String name;
private int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) {
if (age > 0) this.age = age;
}
}
继承(Inheritance)
子类通过 extends 关键字继承父类的属性和方法,实现代码复用,同时可扩展新功能。Java 只支持单继承,但可通过接口实现多继承的类似效果。
class Animal {
public void eat() { System.out.println("动物在吃东西"); }
}
class Dog extends Animal {
public void bark() { System.out.println("汪汪汪"); }
}
多态(Polymorphism)
同一个行为具有不同的表现形式。Java 中的多态分为编译时多态(方法重载)和运行时多态(方法重写 + 父类引用指向子类对象)。
Animal animal = new Dog(); // 向上转型
animal.eat(); // 实际调用 Dog 重写后的 eat 方法(若有)
多态的三个必要条件:继承、重写、父类引用指向子类对象。
1.3 什么是JVM?它的主要组成部分是什么?
JVM(Java Virtual Machine)是运行 Java 字节码的虚拟计算机,屏蔽底层操作系统差异,实现“一次编写,到处运行”。
主要组成部分:
- 类加载器(Class Loader):加载
.class字节码文件到内存。 - 运行时数据区(Runtime Data Area):包括方法区、堆、栈、本地方法栈、程序计数器。
- 执行引擎(Execution Engine):解释/编译字节码,包括解释器、JIT 编译器、垃圾回收器等。
- 本地方法接口(JNI):与本地方法库交互。
1.4 Java中的final关键字有哪些用法?
final 可用于修饰类、方法、变量:
- 修饰类:该类不能被继承,如
String类。 - 修饰方法:该方法不能被子类重写。
- 修饰变量:基本类型变量值不可修改;引用类型变量引用不可变,但对象内部属性可修改。
- 修饰参数:方法参数不可在方法内重新赋值。
public final class FinalClass {} // 不可继承
public void finalMethod() {} // 不可重写
final int x = 10; // 常量,不可修改
1.5 说说equals()方法与==运算符在对象比较上的区别。
==:比较基本类型的值是否相等,或比较引用类型的地址是否相同。equals():Object类中默认实现也是比较地址,但在String、Integer等类中被重写,用于比较对象内容是否相等。自定义类需要重写equals()(同时重写hashCode())来实现内容比较。
String s1 = new String("hello");
String s2 = new String("hello");
s1 == s2; // false,地址不同
s1.equals(s2); // true,内容相同
1.6 String、StringBuffer和StringBuilder的区别是什么?在什么场景下使用哪个类更合适?
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 安全(不可变对象天生安全) | 安全(方法使用 synchronized) | 不安全 |
| 性能 | 拼接产生新对象,效率低 | 效率一般(同步开销) | 效率高 |
使用场景:
- 字符串内容不变或少量拼接,用
String。 - 多线程环境下大量字符串操作,用
StringBuffer。 - 单线程环境下大量字符串操作,用
StringBuilder。
1.7 什么是Java异常处理?请写出try-catch-finally结构,并解释各个部分的作用。
Java 异常处理机制用于捕获、处理和传播运行时错误,主要依赖 try-catch-finally 结构:
try {
// 可能抛出异常的代码
} catch (ExceptionType1 e) {
// 处理异常类型1
} catch (ExceptionType2 e) {
// 处理异常类型2
} finally {
// 无论是否发生异常都会执行的代码(通常用于资源释放)
}
try:包裹可能引发异常的代码块。catch:捕获指定类型的异常,执行相应的处理逻辑。可有多个catch,匹配顺序从上到下。finally:总会被执行(除非 JVM 崩溃或System.exit())。常用于关闭流、连接等资源清理。在 Java 7 之后,可以用try-with-resources自动释放实现了AutoCloseable接口的资源。
1.8 接口和抽象类的区别在哪里?
| 比较点 | 抽象类(abstract class) | 接口(interface) |
|---|---|---|
| 默认方法实现 | 可以有具体方法 | Java 8 起可有默认方法(default)和静态方法 |
| 变量 | 可以声明成员变量 | 只能声明 public static final 常量 |
| 构造器 | 有构造器(供子类调用) | 没有构造器 |
| 继承 | 单继承 | 可实现多个接口 |
| 设计目的 | 表示“是什么”(is-a) | 表示“能做什么”(has-a / can-do) |
选择:当需要共享属性或具体行为时用抽象类;若只定义契约、支持多实现,用接口。
2. 中级部分
2.1 请谈谈Java内存模型(JMM)及其工作原理,包括栈、堆、方法区等各部分的功能。
JMM 规范了多线程环境下共享变量的可见性、有序性和原子性。主要区域:
- 堆(Heap):所有线程共享,存放对象实例和数组。是 GC 主要管理区域。
- 方法区(Method Area):存放类的结构信息(字段、方法、常量池等)。JDK 8 后实现为元空间(Metaspace),使用本地内存。
- 虚拟机栈(VM Stack):线程私有,每个方法执行时创建栈帧,存放局部变量、操作数栈等。递归过深可能导致
StackOverflowError。 - 本地方法栈:为
native方法服务。 - 程序计数器:记录当前线程执行的字节码行号。
JMM 工作要点——线程与主内存交互流程:
flowchart LR
subgraph 主内存
SV["共享变量"]
end
subgraph 线程A
direction TB
TA["线程 A"]
WM_A["工作内存(副本)"]
TA -->|"use(读取值)"| WM_A
WM_A -->|"assign(修改值)"| TA
end
subgraph 线程B
direction TB
TB["线程 B"]
WM_B["工作内存(副本)"]
TB -->|"use(读取值)"| WM_B
WM_B -->|"assign(修改值)"| TB
end
主内存 <-->|"read/load\nstore/write"| WM_A
主内存 <-->|"read/load\nstore/write"| WM_B
流程说明:
- 线程操作变量时,实际是在自己的工作内存中对主内存变量的副本进行
use(读取)和assign(赋值)。 - 变量从主内存到工作内存需要
read和load两步;从工作内存回写到主内存需要store和write两步。 lock/unlock操作由synchronized或ReentrantLock触发,确保加锁期间的原子性与可见性。volatile修饰的变量保证每次use前必须从主内存刷新(load),每次assign后立即store/write回主内存,从而保证了可见性。
2.2 什么是Java集合框架?ArrayList、LinkedList、HashSet之间的差异体现在哪些方面?
集合框架是用于存储和操作对象组的统一架构,主要包括 Collection 接口(及其子接口 List、Set、Queue)和 Map 接口。
核心差异对比:
| 特性 | ArrayList | LinkedList | HashSet |
|---|---|---|---|
| 底层数据结构 | 动态数组 | 双向链表 | HashMap(哈希表) |
| 是否有序 | 有序(按插入顺序) | 有序(按插入顺序) | 无序 |
| 是否允许重复 | 允许 | 允许 | 不允许(基于 hashCode/equals 去重) |
| 是否允许 null | 允许(可多个 null) | 允许(可多个 null) | 允许(最多一个 null) |
| 查询性能 | O(1)(按索引随机访问) | O(n)(需遍历链表) | O(1)(哈希查找) |
| 增删性能 | O(n)(需移动后续元素) | O(1)(定位到节点后直接修改指针) | O(1)(哈希定位后操作) |
| 迭代器类型 | Iterator、ListIterator(可双向遍历) | Iterator、ListIterator(可双向遍历) | Iterator(仅单向) |
| 实现接口 | List、RandomAccess | List、Deque(可作为栈/队列/双端队列) | Set |
| 典型应用场景 | 频繁随机读取、遍历;存储有序可重复数据(如商品列表) | 频繁头部/尾部插入删除;需要队列/栈特性(如任务队列、LRU 缓存) | 快速去重、成员判重;无序集合(如用户 ID 去重、黑名单) |
| 线程安全 | 否(需 Collections.synchronizedList 包装) | 否(需 Collections.synchronizedList 包装) | 否(需 Collections.synchronizedSet 包装或 ConcurrentHashMap.newKeySet()) |
选择建议:
- 频繁随机访问、遍历 → ArrayList (支持
RandomAccess,索引访问极快)。 - 频繁在头部或中间插入/删除 → LinkedList (不需要移动元素,指针操作即可)。
实战场景举例:
在实际项目开发中,根据不同业务需求选择合适的集合类至关重要:
- 订单列表查询场景:在电商后台管理系统中,订单列表需要支持分页展示、按索引快速跳转、频繁的顺序遍历。此时
ArrayList最为合适——它实现了RandomAccess接口,get(index)操作为 O(1),且内存连续、缓存友好。例如:List<Order> orders = new ArrayList<>();存储从数据库查询的订单数据,前端翻页时直接通过索引范围截取即可。
// 初始化:创建订单列表并添加示例数据
List<Order> orders = new ArrayList<>();
orders.add(new Order(1001, "用户A", "已支付"));
orders.add(new Order(1002, "用户B", "待发货"));
orders.add(new Order(1003, "用户C", "已完成"));
orders.add(new Order(1004, "用户D", "已支付"));
// 操作:模拟分页,每页2条,获取第2页(索引2-3)
int pageSize = 2;
int pageNumber = 2;
int fromIndex = (pageNumber - 1) * pageSize;
int toIndex = Math.min(fromIndex + pageSize, orders.size());
List<Order> pageOrders = orders.subList(fromIndex, toIndex);
// 输出:打印分页结果
System.out.println("第 " + pageNumber + " 页订单(共 " + orders.size() + " 条):");
for (int i = 0; i < pageOrders.size(); i++) {
Order o = pageOrders.get(i); // O(1) 按索引访问
System.out.println((fromIndex + i + 1) + ". " + o.id + " - " + o.user + " - " + o.status);
}
// 输出:第 2 页订单(共 4 条):
// 3. 1003 - 用户C - 已完成
// 4. 1004 - 用户D - 已支付
- 消息队列缓冲场景:在消息中间件或任务调度系统中,需要将待处理的任务按 FIFO(先进先出)顺序排队。
LinkedList实现了Deque接口,头尾插入/删除均为 O(1),非常适合模拟队列或栈。例如:Deque<Task> taskQueue = new LinkedList<>();,生产者用offerLast()将任务入队,消费者用pollFirst()取出队头任务执行。
// 初始化:创建任务队列(FIFO)
Deque<String> taskQueue = new LinkedList<>();
// 生产者:将任务加入队尾(offerLast)
taskQueue.offerLast("发送邮件");
taskQueue.offerLast("生成报表");
taskQueue.offerLast("数据备份");
// 消费者:从队头取出任务执行(pollFirst),O(1) 头尾操作
System.out.println("开始处理任务队列(共 " + taskQueue.size() + " 个任务):");
while (!taskQueue.isEmpty()) {
String task = taskQueue.pollFirst(); // 取队头
System.out.println("处理中:" + task);
}
// 输出:开始处理任务队列(共 3 个任务):
// 处理中:发送邮件
// 处理中:生成报表
// 处理中:数据备份
- 用户 ID 去重场景:在营销活动系统中,需要统计参与活动的独立用户数,每个用户 ID 只需记录一次。
HashSet基于哈希表去重,add()和contains()均为 O(1),是去重的首选。例如:Set<Long> uniqueUserIds = new HashSet<>();,遍历活动日志时调用add(userId)即可自动过滤重复 ID,最终size()即为独立用户数。
// 初始化:创建 HashSet 用于去重
Set<Long> uniqueUserIds = new HashSet<>();
// 操作:模拟活动日志,遍历并添加用户 ID(重复的自动去重)
Long[] activityLog = {1001L, 1002L, 1001L, 1003L, 1002L, 1004L};
for (Long userId : activityLog) {
boolean added = uniqueUserIds.add(userId); // O(1) 添加 & 去重
System.out.println("用户 " + userId + " -> " + (added ? "首次参与" : "重复参与(已去重)"));
}
// 输出:统计独立用户数
System.out.println("参与活动的独立用户数:" + uniqueUserIds.size());
// 输出:用户 1001 -> 首次参与
// 用户 1002 -> 首次参与
// 用户 1001 -> 重复参与(已去重)
// 用户 1003 -> 首次参与
// 用户 1002 -> 重复参与(已去重)
// 用户 1004 -> 首次参与
// 参与活动的独立用户数:4
多线程环境下的线程安全替代方案:
上述三种集合类均非线程安全,在多线程并发场景下需选用对应的安全替代方案:
-
ArrayList→ 若读多写少,推荐CopyOnWriteArrayList(写时复制,读无锁);或使用Collections.synchronizedList(new ArrayList<>())包装。 -
LinkedList→ 推荐ConcurrentLinkedDeque,基于 CAS 的无锁双端队列,适用于高并发生产者-消费者模型。 -
HashSet→ 推荐ConcurrentHashMap.newKeySet()(JDK 8+),底层基于ConcurrentHashMap,分段锁设计,并发性能优秀;或Collections.synchronizedSet(new HashSet<>())包装。 -
对于键值对场景,
ConcurrentHashMap是HashMap的线程安全替代,采用 CAS + synchronized 分段机制,在读多写少场景下性能远优于Hashtable或Collections.synchronizedMap。 -
需要集合元素唯一、快速判重 → HashSet (O(1) 级别的
contains检查)。
load 载入工作内存的副本。
2. 线程对工作内存中的副本进行 use/assign 操作。
3. 修改后的值通过 store 写回主内存,最后执行 write 到共享变量。
4. lock/unlock 操作由 synchronized 或 ReentrantLock 触发,确保原子性与可见性。
5. volatile 变量保证每次 use 前强制 refresh,每次 assign 后立即 store/write。
2.2 什么是Java集合框架?ArrayList、LinkedList、HashSet之间的差异体现在哪些方面?
集合框架是用于存储和操作对象组的统一架构,主要包括 Collection 接口(及其子接口 List、Set、Queue)和 Map 接口。
ArrayList:
- 底层:动态数组,查询快 O(1),增删慢(需移动元素)。
- 线程不安全,可存储重复元素,自动扩容。
LinkedList:
- 底层:双向链表,增删快 O(1)(定位到节点后),查询慢 O(n)。
- 实现了
Deque,可作为栈/队列使用。
HashSet:
- 底层:基于
HashMap,元素无序,不允许重复。 - 利用
hashCode和equals判断重复,适合快速去重和查找 O(1)。
选择:需随机访问选 ArrayList;频繁插入删除选 LinkedList;去重和无序选 HashSet。
2.3 synchronized关键字如何实现线程同步?还有哪些其他方式可以实现线程安全?
synchronized 基于对象监视器(Monitor),通过互斥锁保证同一时刻只有一个线程执行同步代码块或方法。
实现方式:
- 修饰实例方法:锁当前对象。
- 修饰静态方法:锁类的 Class 对象。
- 修饰代码块:指定锁对象。
其他线程安全方式:
ReentrantLock:可重入锁,支持公平锁、条件等待、可中断获取。volatile:保证变量可见性和禁止指令重排,但不保证原子性。- 原子类(
AtomicInteger等):基于 CAS 实现无锁线程安全。 ThreadLocal:线程本地变量,隔离数据。- 并发集合:
ConcurrentHashMap、CopyOnWriteArrayList等。
2.4 请解释一下Java中的四种引用类型(强引用、软引用、弱引用、虚引用)及应用场景。
| 引用类型 | 回收时机 | 应用场景 |
|---|---|---|
| 强引用(Strong) | 绝不回收 | 正常的对象引用,如 Object obj = new Object() |
| 软引用(Soft) | 内存不足时回收 | 缓存敏感数据,如图片缓存 |
| 弱引用(Weak) | 下次 GC 时回收 | WeakHashMap、ThreadLocal 键 |
| 虚引用(Phantom) | 任何时候都可能被回收,需与引用队列配合 | 跟踪对象回收状态,管理直接内存(如 NIO 的 Cleaner) |
2.5 如何避免死锁?你怎样分析和解决多线程环境下的死锁问题?
避免死锁策略:
- 按固定顺序获取锁。
- 使用
tryLock()获取锁,设置超时时间。 - 减少锁的粒度和持有时间。
- 使用
java.util.concurrent包下的高级工具,如ConcurrentHashMap、CountDownLatch等非锁并发控制。
分析与解决:
- 通过
jstack或 VisualVM 导出线程 dump,查看线程状态(BLOCKED)和锁占用关系,定位循环等待。 - 在代码中显式使用可中断的锁获取方式,或使用
ReentrantLock的getOwner()、getQueuedThreads()监控。
2.6 请阐述Java反射机制,并给出一个简单的反射实例。
反射机制允许程序在运行时获取类的内部信息(构造器、字段、方法等),并动态操作对象。
简单实例:
// 获取 Class 对象
Class<?> clazz = Class.forName("java.util.ArrayList");
// 创建实例
Object list = clazz.getDeclaredConstructor().newInstance();
// 获取 add 方法并调用
Method add = clazz.getMethod("add", Object.class);
add.invoke(list, "Hello");
// 查看 size
Method size = clazz.getMethod("size");
System.out.println(size.invoke(list)); // 输出 1
3. 高级部分
3.1 Java并发编程中的CAS原理是什么?请描述AQS(AbstractQueuedSynchronizer)的工作原理及其在Java并发包中的应用。
CAS(Compare And Swap)
一种无锁算法,包含三个操作数:内存值 V、旧的预期值 A、更新值 B。当且仅当 V == A 时,将 V 更新为 B。由 CPU 指令(如 cmpxchg)原子地完成,JUC 原子类基于此实现。
AQS(AbstractQueuedSynchronizer)
基于 FIFO 等待队列的同步器框架,核心是 state 变量和 CLH 变体队列。线程获取锁失败后进入队列等待,释放锁时唤醒后继节点。通过模板方法模式实现 tryAcquire/tryRelease 等完成自定义同步组件。
应用:ReentrantLock(排他锁)、Semaphore(共享锁)、CountDownLatch、CyclicBarrier、ReentrantReadWriteLock 等。
3.2 请详细介绍一下Java的垃圾回收机制(GC),并谈谈分代收集、CMS、G1等垃圾回收器的特点和适用场景。
GC 自动回收不再使用的对象,主要判断算法:引用计数(已弃用)和可达性分析。
分代收集理论:根据对象生命周期分为新生代(Eden, Survivor)和老年代。新生代使用复制算法(Minor GC),老年代使用标记-清除或标记-整理(Full GC)。
常见垃圾回收器:
| 回收器 | 特点 | 适用场景 |
|---|---|---|
| CMS | 并发标记、低停顿,基于标记-清除,会产生碎片 | 老年代,追求低延迟的应用 |
| G1 | 整体标记-整理,局部复制,可预测停顿,无碎片 | 大堆、多处理器,需指定停顿时间 |
| ZGC | 基于区域,极低停顿(<10ms),支持大堆 | 大内存、低延迟场景 |
其他:Parallel GC 追求吞吐量;Serial GC 单线程,适合客户端。
3.3 对于Spring框架的理解,你能描述一下IoC(控制反转)和DI(依赖注入)的概念吗?并简述AOP(面向切面编程)的应用场景。
IoC:将对象创建和依赖关系的管理权从程序代码转移给 Spring 容器,通过配置或注解定义 Bean,容器负责装配。
DI:IoC 的具体实现方式,包括构造器注入、setter 注入、字段注入(@Autowired)。Spring 通过反射或 CGLIB 代理完成注入。
AOP:将横切关注点(日志、事务、安全等)从业务逻辑中分离,通过切面编程增强功能。应用场景:
- 声明式事务管理
@Transactional - 权限校验
- 日志记录
- 缓存处理
Spring AOP 基于动态代理(JDK 或 CGLIB),AspectJ 提供更强大的编译期/加载期织入。
3.4 在Java 8及以上版本中,Lambda表达式和函数式接口是如何工作的?它们对Java编程有什么影响?
Lambda 表达式:一种简洁的匿名函数写法,语法 (参数) -> { 方法体 },本质是函数式接口的实例。编译器通过 invokedynamic 指令在运行时生成函数式接口实现类,避免编译期生成匿名内部类,减少内存占用。
函数式接口:只有一个抽象方法的接口,可以用 @FunctionalInterface 注解标记。常见接口:Predicate<T>、Consumer<T>、Function<T,R>、Supplier<T>。
// 传统匿名内部类
new Thread(() -> System.out.println("Hello")).start();
影响:
- 简化代码,推动函数式编程风格。
- 配合 Stream API 极大提升集合处理的表达力和可读性。
Optional类减少空指针,方法引用(::)增强代码复用。
3.5 JVM调优通常涉及哪些方面?请列举一些常见的JVM调优参数以及它们的作用。
JVM 调优围绕内存、GC、线程、编译等方面:
常见参数:
| 参数 | 作用 |
|---|---|
-Xms | 初始堆大小 |
-Xmx | 最大堆大小,建议与 Xms 一致避免动态调整开销 |
-Xss | 线程栈大小 |
-XX:NewRatio | 新生代与老年代比例 |
-XX:SurvivorRatio | Eden 与 Survivor 区比例 |
-XX:+UseG1GC | 启用 G1 垃圾回收器 |
-XX:MaxGCPauseMillis | 期望的最大 GC 暂停时间(G1 目标) |
-XX:+PrintGCDetails | 打印 GC 详细信息 |
-XX:MetaspaceSize | 元空间初始大小,避免频繁 Full GC |
-XX:MaxMetaspaceSize | 最大元空间大小 |
调优步骤:
- 监控内存、GC 频率和停顿时间。
- 根据应用特点选择合适的 GC 收集器。
- 逐步调整堆大小、代比例,确保不频繁 Full GC。
- 分析堆转储快照排查内存泄漏。
以上内容梳理了 Java 面试中常见的核心知识点,建议结合代码实践与项目经验深入理解每个概念。不同级别的题目可根据应聘岗位侧重准备。
面试准备建议
-
结构化回答,层层递进
面对每一个知识点,可以采用“概念定义 → 底层原理 → 代码示例 → 实际场景”的四步回答法。例如,解释 JMM 时,先说明它是规范,再画出主内存与工作内存的交互,配以volatile或锁的代码片段,最后结合自己项目中的并发优化经验收尾。这样既能展示理论深度,又能体现工程实践。 -
主动关联项目经验
无论题目大小,都尽量落地到真实项目。比如被问到集合框架时,除了对比 ArrayList 与 LinkedList,还要补充“在订单列表查询场景中选了 ArrayList,因为需要频繁随机读取;而在消息中间件的缓冲队列中用 LinkedList 模拟了 FIFO 队列”。面试官更看重你为什么这么选以及遇到过什么坑。 -
锁定高频考点,对比记忆
本文覆盖的基础类型、集合框架、多线程、JVM 内存/GC、Spring IoC/AOP、Lambda/Stream 等模块都是面试重灾区。建议用表格或思维导图对比相似概念(如StringBuffervsStringBuilder、synchronizedvsReentrantLock、CMS vs G1),强化“区别 → 场景 → 选型”逻辑链。 -
模拟面试 + 定期复盘
利用在线编程平台(如 LeetCode 多线程题目)实战并发编程;对着白板讲述 GC 分代流程;与同学互相模拟问答,录音后复盘表达是否清晰。根据目标岗位的级别侧重点,制定分模块的复习计划,确保基础知识扎实、高级话题有思考。
结合本文梳理的全部要点,坚持“理解原理、动手实践、讲出故事”,你将在 Java 面试中更有竞争力。