跳到主要内容
Java 常见面试题及答案汇总 | 极客日志
Java java 算法
Java 常见面试题及答案汇总 Java 面试核心覆盖基础语法、集合框架、多线程并发、JVM 内存模型、Spring 原理及数据库优化。内容深入解析了 String 不可变性、HashMap 哈希冲突解决、线程池参数配置、GC 收集器选择及 Spring AOP 动态代理机制。结合代码示例演示了单例模式实现、事务失效排查及 MyBatis 缓存策略,旨在帮助开发者系统掌握高频考点与底层逻辑,提升实战能力。
KernelLab 发布于 2026/3/20 更新于 2026/4/24 2 浏览一、Java 基础语法与核心特性
1. Java 的核心特性有哪些?
跨平台性 :通过 JVM(Java 虚拟机)实现,字节码文件可在任意支持 JVM 的操作系统运行;
面向对象 :封装、继承、多态三大核心特性;
安全性 :支持沙箱机制、字节码校验、权限控制(如文件 IO 权限);
健壮性 :自动垃圾回收(GC)避免内存泄漏,强类型检查、异常处理机制减少运行时错误;
分布式 :支持 RMI(远程方法调用)、HTTP 协议,便于开发分布式应用;
多线程 :内置多线程 API,支持并发编程。
2. 基本数据类型与包装类的区别?
维度 基本数据类型(如 int、float) 包装类(如 Integer、Float) 本质 原始值,无对象属性 引用类型,继承 Object 类 默认值 有(如 int 默认 0,boolean 默认 false) 无,默认 null 适用场景 简单运算、局部变量,效率高 集合框架(如 List)、泛型、需要 null 值的场景 缓存机制 无 部分包装类(Integer[-128~127]、Byte、Short 等)有常量池缓存
关键考点 :
自动装箱/拆箱 :Java 5+ 特性,编译器自动完成基本类型与包装类的转换(如 int i = new Integer(10)→拆箱,Integer j = 10→装箱);
缓存陷阱 :Integer a = 127; Integer b = 127; → a == b为 true(复用缓存);Integer c = 128; Integer d = 128; → c == d为 false(新建对象),需用 equals() 比较值。
3. String、StringBuffer、StringBuilder 的区别?
核心差异在于可变性 和线程安全 :
String :不可变(底层是 final 修饰的 char 数组/JDK9+ byte 数组),每次修改都会创建新对象,效率低;
StringBuffer :可变,线程安全(方法加 synchronized 锁),适用于多线程环境的字符串拼接;
StringBuilder :可变,线程不安全,效率高于 StringBuffer,适用于单线程环境的字符串拼接。
底层原理 :
String 的不可变性源于 private final char value[](JDK8),final 修饰数组引用不可变,且数组无暴露修改接口;StringBuffer 和 StringBuilder 继承 AbstractStringBuilder,底层是可变 char 数组,扩容机制为:默认初始容量 16,当长度超过容量时,新容量=原容量×2+2,不足则直接扩容到所需长度。
4. final 关键字的三种用法?
修饰类 :类不可被继承(如 String、Math),子类无法扩展其功能;
修饰方法 :方法不可被重写,可防止子类修改父类核心逻辑;
修饰变量 :变量不可被重新赋值(基本类型:值不可变;引用类型:引用地址不可变,但对象内容可修改)。易错点 :
final int[] arr = {1,2,3}; arr[0] = 4; 合法(数组内容可变);arr = new int[5]; 非法(引用地址不可变)。
5. 接口(Interface)与抽象类(Abstract Class)的区别? 维度 接口(Interface) 抽象类(Abstract Class) 继承方式 多实现(一个类可实现多个接口) 单继承(一个类只能继承一个抽象类) 成员变量 只能是 public static final 常量 可包含普通变量、静态变量、常量 成员方法 JDK8 前:只能是抽象方法;JDK8+:支持 default/static 方法;JDK9+:支持 private 方法 可包含抽象方法、普通方法、静态方法 构造方法 无 有(不能实例化,供子类调用) 设计目的 定义行为规范,解耦(如 List 接口) 定义类的模板,复用代码(如 HttpServlet)
接口 :不同类需统一行为但实现不同(如 Runnable 接口);
抽象类 :同类组件共享核心逻辑(如 AbstractList 封装 List 的公共方法)。
6. Java 异常体系的核心结构?
顶层父类 :Throwable,包含两个核心子类:
Error:严重错误(如 OutOfMemoryError、StackOverflowError),程序无法恢复,无需捕获;
Exception:可处理的异常,分为:
受检异常(Checked Exception):编译时必须捕获(如 IOException、SQLException);
非受检异常(Unchecked Exception):运行时异常(如 NullPointerException、ArrayIndexOutOfBoundsException),继承自 RuntimeException,无需强制捕获。
try:包裹可能抛出异常的代码;
catch:捕获并处理异常(可多个 catch,按异常子类→父类顺序);
finally:无论是否抛出异常,都会执行(常用于关闭资源,如流、数据库连接);
throw:手动抛出异常(如 throw new IllegalArgumentException("参数非法"));
throws:声明方法可能抛出的异常,告知调用者。
避免捕获 Throwable(包含 Error,无法处理);
不要忽略异常(空 catch 块);
优先使用 try-with-resources 自动关闭资源(JDK7+,支持实现 AutoCloseable 接口的类)。
二、Java 集合框架
1. 集合框架的核心接口与继承关系? Java 集合框架核心分为两大体系(均位于 java.util 包):
单列集合(Collection) :存储单个元素,核心子接口:
List:有序、可重复(如 ArrayList、LinkedList、Vector);
Set:无序、不可重复(如 HashSet、TreeSet、LinkedHashSet);
双列集合(Map) :存储键值对(key-value),核心实现类 HashMap、TreeMap、LinkedHashMap、ConcurrentHashMap。
List:支持索引访问,可通过 get(int index) 获取元素;
Set:基于 equals() 和 hashCode() 保证元素唯一性;
Map:key 不可重复(重复会覆盖 value),value 可重复;JDK8+ 中 Map 提供 forEach()、computeIfAbsent() 等便捷方法。
2. ArrayList 与 LinkedList 的区别? 维度 ArrayList(数组实现) LinkedList(双向链表实现) 底层结构 动态数组(Object[]) 双向链表(每个节点存储 prev、next、value) 访问效率 随机访问快(O(1)),通过索引直接定位 随机访问慢(O(n)),需遍历链表 增删效率 尾部增删快(O(1)),中间增删慢(需移动数组元素,O(n)) 中间增删快(O(1),只需修改节点指针),尾部增删需遍历到末尾(O(n),可通过 last 指针优化) 内存占用 连续内存,占用少(无额外指针开销) 非连续内存,每个节点有额外指针开销 线程安全 不安全 不安全
ArrayList :频繁查询、少量增删(如数据展示列表);
LinkedList :频繁中间增删、队列/栈实现(如消息队列)。
3. HashMap 的底层实现原理(JDK1.7 vs JDK1.8)? HashMap 是基于'哈希表'的 Map 实现,核心是'数组 + 链表/红黑树'的结构,目的是平衡查询和增删效率。
JDK1.7 实现:
底层 :数组(Entry[])+ 单向链表;
存储流程 :
计算 key 的 hashCode() → 经过哈希扰动(hashCode() ^ (hashCode() >>> 16))得到哈希值;
哈希值 & 数组长度 -1 → 定位数组索引(保证索引在数组范围内);
若索引位置无元素,直接存储;若有元素(哈希冲突),采用'头插法'插入链表。
JDK1.8 优化:
底层 :数组(Node[])+ 单向链表 + 红黑树(链表长度≥8 且数组长度≥64 时,链表转为红黑树;链表长度≤6 时,红黑树转回链表);
存储流程 :哈希扰动逻辑不变,哈希冲突时采用'尾插法'插入链表(避免 JDK1.7 头插法导致的链表循环问题);
其他优化 :
扩容机制:默认初始容量 16,负载因子 0.75,当元素个数≥容量×负载因子时,触发扩容(新容量=原容量×2);
支持 null key 和 null value(null key 的 hash 值为 0,存储在数组索引 0 位置)。
哈希扰动:减少哈希值的高位忽略问题,提升哈希分布均匀性;
链地址法:冲突元素以链表/红黑树形式存储在同一索引位置。
线程安全问题 :
HashMap 线程不安全,多线程环境下可能出现:
JDK1.7:扩容时头插法导致链表循环;
JDK1.8:put 操作可能覆盖数据。
解决方案:使用 ConcurrentHashMap 或 Collections.synchronizedMap(new HashMap<>())。
4. ConcurrentHashMap 的线程安全实现(JDK1.7 vs JDK1.8)? ConcurrentHashMap 是 HashMap 的线程安全版本,核心差异在于锁机制:
JDK1.7 实现:
底层 :Segment 数组 + HashEntry 数组 + 链表;
锁机制 :分段锁(Segment 继承 ReentrantLock),每个 Segment 对应一把锁,仅锁定当前 Segment,支持多线程并发访问不同 Segment,提高并发度(默认 Segment 数量 16,支持 16 个线程并发)。
JDK1.8 优化:
底层 :Node 数组 + 链表 + 红黑树(与 HashMap 结构一致);
锁机制 :放弃分段锁,采用'CAS + synchronized'实现线程安全:
无冲突时:CAS 操作原子性插入元素;
有冲突时:synchronized 锁定当前链表/红黑树的头节点,仅锁定冲突节点所在的链表/树,并发度更高;
其他优化 :支持 computeIfAbsent()、forEach() 等原子操作,性能优于 JDK1.7。
5. HashSet 的实现原理? HashSet 底层依赖 HashMap 实现,核心逻辑:
HashSet 的构造方法会创建一个 HashMap 实例;
存储元素时:add(E e) → 调用 HashMap 的 put(e, PRESENT),其中 PRESENT 是一个静态空 Object(仅占位,不存储实际值);
元素唯一性:依赖 HashMap 的 key 不可重复特性(通过 equals() 和 hashCode() 判断);
特性:无序、不可重复、线程不安全,查询/增删效率 O(1)(无哈希冲突时)。
关键考点 :
自定义对象作为 HashSet 元素时,必须重写 equals() 和 hashCode(),否则无法保证唯一性(默认使用 Object 类的方法,比较对象地址)。重写原则:
两个对象 equals() 返回 true → hashCode() 必须相等;
两个对象 hashCode() 相等 → equals() 不一定返回 true(哈希冲突)。
三、Java 多线程与并发
1. Java 创建线程的三种方式? 方式 1:继承 Thread 类
重写 run() 方法(线程执行逻辑),调用 start() 方法启动线程(底层调用 start0() native 方法创建操作系统线程);
class MyThread extends Thread {
@Override
public void run () {
System.out.println("Thread running" );
}
}
new MyThread ().start();
方式 2:实现 Runnable 接口
重写 run() 方法,将实例传入 Thread 类启动;
class MyRunnable implements Runnable {
@Override
public void run () {
System.out.println("Runnable running" );
}
}
new Thread (new MyRunnable ()).start();
方式 3:实现 Callable 接口
重写 call() 方法(支持返回值和抛出异常),通过 FutureTask 包装后传入 Thread 启动;
class MyCallable implements Callable <String> {
@Override
public String call () throws Exception {
return "Callable result" ;
}
}
FutureTask<String> future = new FutureTask <>(new MyCallable ());
new Thread (future).start();
String result = future.get();
继承 Thread :无法继承其他类(Java 单继承),代码简单;
实现 Runnable/Callable :可继承其他类,支持多线程共享资源,Callable 支持返回值和异常处理。
2. 线程的生命周期与状态转换? Java 线程有 6 种状态(定义在 Thread.State 枚举中),状态转换如下:
NEW:线程创建后未启动(未调用 start());
RUNNABLE:线程启动后,处于可运行状态(包含操作系统的'运行中'和'就绪');
BLOCKED:线程等待同步锁(如 synchronized 未获取锁时);
WAITING:线程无限期等待(如调用 Object.wait()、Thread.join()、LockSupport.park(),需其他线程唤醒);
TIMED_WAITING:线程限时等待(如调用 Thread.sleep(ms)、Object.wait(ms)、Thread.join(ms),超时自动唤醒);
TERMINATED:线程执行完成或异常终止。
核心转换路径 :
NEW → RUNNABLE(start()) → TERMINATED(执行完成);
RUNNABLE → BLOCKED(竞争锁失败) → RUNNABLE(获取锁);
RUNNABLE → WAITING/TIMED_WAITING(调用等待方法) → RUNNABLE(被唤醒/超时)。
3. synchronized 与 Lock 的区别? 维度 synchronized(内置锁) Lock(显式锁,如 ReentrantLock) 锁实现 JVM 层面实现(C++ 代码) JDK 层面实现(Java 代码) 锁获取与释放 自动获取(进入同步块)、自动释放(退出同步块/异常) 手动获取(lock())、手动释放(unlock(),需在 finally 中执行) 锁类型 可重入锁、非公平锁(默认),JDK6+ 支持偏向锁/轻量级锁/重量级锁升级 可重入锁,支持公平锁/非公平锁(构造函数指定) 功能扩展 无(仅支持基础同步) 支持中断锁(lockInterruptibly())、超时锁(tryLock(ms))、条件变量(Condition)、读写锁(ReentrantReadWriteLock) 性能 JDK6+ 优化后,性能接近 Lock 高并发场景下性能更优,灵活度高
synchronized :简单同步场景(如单例模式、简单方法同步),代码简洁,无需手动管理锁;
Lock :复杂并发场景(如超时获取锁、中断锁、读写分离),如缓存系统、分布式锁实现。
4. volatile 关键字的作用? volatile 是 Java 提供的轻量级同步机制,核心作用有两个:
保证可见性 :一个线程修改 volatile 变量后,其他线程能立即看到最新值(禁止 CPU 缓存,变量读写直接操作主内存);
禁止指令重排序 :编译器和 CPU 会对指令重排序优化,volatile 通过内存屏障(Memory Barrier)阻止重排序(如 DCL 单例模式中,volatile 修饰实例变量防止指令重排导致的空指针)。
不保证原子性 :如 volatile int i = 0; i++; 非原子操作(包含读取、加 1、写入三步),多线程下可能出现计数错误,需配合 synchronized 或 AtomicInteger 使用;
不能替代锁 :仅适用于'单写多读'或'状态标记'场景(如 volatile boolean flag = false; 控制线程启停)。
public class Singleton {
private static volatile Singleton instance;
private Singleton () {}
public static Singleton getInstance () {
if (instance == null ) {
synchronized (Singleton.class) {
if (instance == null ) {
instance = new Singleton ();
}
}
}
return instance;
}
}
5. 线程池的核心参数与工作原理? Java 线程池核心类是 ThreadPoolExecutor,基于'池化思想'减少线程创建/销毁开销,提高并发效率。
核心参数(构造方法): public ThreadPoolExecutor (
int corePoolSize, // 核心线程数(常驻线程,即使空闲也不销毁)
int maximumPoolSize, // 最大线程数(核心线程 + 临时线程的总上限)
long keepAliveTime, // 临时线程空闲时间(超过则销毁)
TimeUnit unit, // keepAliveTime 的时间单位
BlockingQueue<Runnable> workQueue, // 任务阻塞队列(核心线程满时,任务入队)
ThreadFactory threadFactory, // 线程创建工厂(自定义线程名称、优先级等)
RejectedExecutionHandler handler // 拒绝策略(队列和最大线程数都满时,处理新任务)
)
工作原理:
提交任务时,若核心线程数未满,创建核心线程执行任务;
核心线程满时,任务加入阻塞队列;
队列满时,若未达到最大线程数,创建临时线程执行任务;
临时线程空闲时间超过 keepAliveTime,销毁临时线程;
队列和最大线程数都满时,执行拒绝策略。
常见拒绝策略:
AbortPolicy(默认):直接抛出 RejectedExecutionException;
CallerRunsPolicy:由提交任务的线程(调用者)执行任务;
DiscardPolicy:直接丢弃新任务,无异常;
DiscardOldestPolicy:丢弃队列中最旧的任务,加入新任务。
常见线程池(Executors 工具类):
Executors.newFixedThreadPool(n):固定核心线程数和最大线程数(n),队列无界(LinkedBlockingQueue);
Executors.newCachedThreadPool():核心线程数 0,最大线程数 Integer.MAX_VALUE,临时线程空闲 60 秒销毁,队列同步移交(SynchronousQueue);
Executors.newSingleThreadExecutor():核心线程数 1,最大线程数 1,队列无界,保证任务串行执行;
Executors.newScheduledThreadPool(n):核心线程数 n,支持定时/延迟执行任务(ScheduledFutureTask)。
注意 :阿里巴巴 Java 开发手册禁止使用 Executors 创建线程池,原因是:
newFixedThreadPool/newSingleThreadExecutor:队列无界,可能导致 OOM;
newCachedThreadPool:最大线程数无界,可能创建大量线程导致 CPU/内存耗尽。
推荐直接使用 ThreadPoolExecutor 构造方法,指定合理参数(如核心线程数=CPU 核心数±1,队列使用有界队列)。
6. ThreadLocal 的原理与内存泄漏问题? ThreadLocal 是线程本地存储工具,允许每个线程拥有独立的变量副本,避免多线程共享变量的并发问题。
原理:
底层结构 :每个 Thread 对象持有一个 ThreadLocalMap(ThreadLocal 的内部类),ThreadLocalMap 的 key 是 ThreadLocal 实例(弱引用),value 是线程本地变量副本;
核心方法 :
set(T value):获取当前线程的 ThreadLocalMap,将(当前 ThreadLocal 实例,value)存入;
get():获取当前线程的 ThreadLocalMap,根据当前 ThreadLocal 实例获取 value,无则调用 initialValue() 初始化;
remove():删除当前线程的 ThreadLocalMap 中对应的键值对。
内存泄漏问题:
原因 :ThreadLocalMap 的 key 是弱引用(WeakReference<ThreadLocal<?>>),当 ThreadLocal 实例被回收(如外部引用置 null),key 会变成 null,而 value 是强引用,若线程未结束(如线程池核心线程),value 无法被 GC 回收,导致内存泄漏;
解决方案 :
用完 ThreadLocal 后调用 remove() 方法删除 value;
避免使用静态 ThreadLocal(生命周期长,易导致内存泄漏);
线程池场景下,确保任务执行完成后清理 ThreadLocal 变量。
存储线程上下文信息(如用户登录信息、数据库连接、事务对象);
避免方法参数传递(如 Spring 的 RequestContextHolder 底层使用 ThreadLocal 存储 HttpServletRequest)。
四、JVM 核心原理
1. JVM 内存模型(运行时数据区)? JVM 运行时数据区分为 5 个部分(基于 JDK8):
2. 垃圾回收(GC)的核心原理? GC 是 JVM 自动回收堆中无用对象(无引用的对象)的过程,核心目标是释放内存,避免内存泄漏。
1. 垃圾判定算法:
引用计数法 :给对象添加引用计数器,引用 +1,引用失效 -1,计数器为 0 则判定为垃圾;缺点:无法解决循环引用(如 A 引用 B,B 引用 A,计数器均为 1,无法回收);
可达性分析算法(JVM 采用) :以'GC Roots'为起点,遍历对象引用链,不可达的对象判定为垃圾;
GC Roots 包括:虚拟机栈局部变量表中的引用、本地方法栈中的引用、方法区静态变量引用、常量池引用、活跃线程的引用。
2. 常见 GC 算法:
标记 - 清除算法(Mark-Sweep) :
步骤:标记垃圾对象→清除垃圾对象;
优点:简单高效;
缺点:产生内存碎片,后续大对象分配可能失败。
复制算法(Copying) :
步骤:将内存分为两块(如 Eden 和 S0/S1),标记存活对象→复制到另一块内存,清除原内存;
优点:无内存碎片,分配效率高;
缺点:内存利用率低(仅 50%),适合年轻代(存活对象少)。
标记 - 整理算法(Mark-Compact) :
步骤:标记存活对象→将存活对象向内存一端移动→清除另一端垃圾;
优点:无内存碎片,内存利用率高;
缺点:移动对象成本高,适合老年代(存活对象多)。
分代收集算法(JVM 采用) :
原理:根据对象存活时间划分代(年轻代、老年代),不同代采用不同 GC 算法;
年轻代:存活对象少,采用复制算法;
老年代:存活对象多,采用标记 - 清除或标记 - 整理算法。
3. 常见 GC 收集器:
Serial 收集器 :单线程 GC,年轻代采用复制算法,老年代采用标记 - 整理算法,适合单 CPU 环境(如客户端应用);
Parallel Scavenge 收集器 :多线程 GC,年轻代复制算法,追求高吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+GC 时间)),适合服务器应用;
ParNew 收集器 :Parallel Scavenge 的多线程版本,支持与 CMS 收集器配合;
CMS 收集器(Concurrent Mark Sweep) :老年代 GC,基于标记 - 清除算法,并发收集(与用户线程同时执行),追求低延迟,适合响应时间敏感的应用(如 Web 应用);缺点:产生内存碎片、并发开销大;
G1 收集器(Garbage-First) :JDK9+ 默认 GC,基于标记 - 整理算法,将堆划分为多个大小相等的 Region,优先回收垃圾多的 Region,兼顾吞吐量和低延迟,支持大堆内存(如数十 GB);
ZGC/Shenandoah 收集器 :新一代低延迟 GC,暂停时间控制在毫秒级以下,支持 TB 级堆内存。
3. 类加载机制与双亲委派模型? 类加载是将 .class 字节码文件加载到 JVM 内存,生成 Class 对象的过程,核心分为 5 个阶段:
1. 类加载流程:
加载(Loading) :通过类加载器读取 .class 文件,生成二进制字节流,在堆中创建 Class 对象;
验证(Verification) :校验字节码合法性(如文件格式、语法、语义、符号引用验证),防止恶意字节码;
准备(Preparation) :为类静态变量分配内存并设置默认值(如 int 默认 0,boolean 默认 false),不包含实例变量;
解析(Resolution) :将符号引用(如类名、方法名)转换为直接引用(内存地址);
初始化(Initialization) :执行类构造器 <clinit>() 方法(静态变量赋值 + 静态代码块执行),初始化顺序:父类→子类,静态变量→静态代码块。
2. 类加载器分类:
启动类加载器(Bootstrap ClassLoader) :C++ 实现,加载 JDK 核心类库(如 rt.jar),无父加载器;
扩展类加载器(Extension ClassLoader) :Java 实现,加载 jre/lib/ext 目录下的类库;
应用程序类加载器(Application ClassLoader) :Java 实现,加载应用 classpath 下的类库(默认类加载器);
自定义类加载器(Custom ClassLoader) :继承 ClassLoader 类,重写 findClass() 方法,用于加载自定义路径的类(如热部署、加密类)。
3. 双亲委派模型:
核心规则 :类加载器加载类时,先委托父加载器加载,父加载器无法加载(找不到类)时,才由自身加载;
流程 :应用程序类加载器→扩展类加载器→启动类加载器(顶层),若启动类加载器无法加载,反向逐级尝试加载;
作用 :
避免类重复加载(如 java.lang.String 仅由启动类加载器加载一次);
保护核心类库(防止自定义 java.lang.String 类替换核心类)。
破坏双亲委派模型的场景 :
热部署(如 OSGi 框架):需要不同模块加载同一类的不同版本;
JNDI、SPI 机制(如 JDBC 驱动加载):核心类由启动类加载器加载,需加载应用 classpath 下的驱动类,通过线程上下文类加载器(Thread Context ClassLoader)实现。
五、Spring 核心框架
1. Spring IoC 的原理与实现? IoC(Inversion of Control,控制反转)是 Spring 的核心思想,指将对象的创建、依赖注入(DI)的控制权从应用程序转移到 Spring 容器,实现解耦。
核心概念:
IoC 容器 :Spring 的核心组件(如 ApplicationContext、BeanFactory),负责管理 Bean 的生命周期(创建、初始化、销毁)和依赖关系;
Bean :IoC 容器管理的对象(如 Service、Dao 层对象);
依赖注入(DI) :IoC 的具体实现,容器在创建 Bean 时自动注入其依赖的其他 Bean(无需手动 new 对象)。
依赖注入的三种方式: 字段注入 :直接在字段上添加 @Autowired 注解,代码简洁,但不推荐(无法通过构造方法校验依赖,不利于单元测试);
@Service
public class UserService {
@Autowired
private UserDao userDao;
}
Setter 方法注入 :通过 Setter 方法注入依赖,适用于可选依赖;
@Service
public class UserService {
private UserDao userDao;
@Autowired
public void setUserDao (UserDao userDao) {
this .userDao = userDao;
}
}
构造方法注入 :通过 Bean 的构造方法传入依赖,推荐使用(强制依赖,避免空指针);
@Service
public class UserService {
private final UserDao userDao;
public UserService (UserDao userDao) {
this .userDao = userDao;
}
}
IoC 容器初始化流程:
加载配置文件(如 XML、注解 @Configuration);
解析配置,扫描 Bean 定义(如 @Component、@Service、@Repository),注册到 BeanDefinitionRegistry;
实例化 Bean(默认单例,懒加载除外);
依赖注入(DI):通过 BeanPostProcessor(后置处理器)自动注入依赖;
初始化 Bean:执行 @PostConstruct 注解方法、InitializingBean 接口的 afterPropertiesSet() 方法、自定义 init-method;
Bean 就绪,供应用程序调用;
容器关闭时,销毁 Bean:执行 @PreDestroy 注解方法、DisposableBean 接口的 destroy() 方法、自定义 destroy-method。
2. Spring AOP 的原理与应用? AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 的核心特性,通过'横切'机制,将日志、事务、权限等通用功能(切面)与业务逻辑解耦,实现代码复用。
核心概念:
切面(Aspect) :封装通用功能的类(如日志切面、事务切面),包含通知和切入点;
通知(Advice) :切面的具体逻辑(如日志打印、事务提交),分为 5 种类型:
@Before:目标方法执行前执行;
@After:目标方法执行后执行(无论是否异常);
@AfterReturning:目标方法正常返回后执行;
@AfterThrowing:目标方法抛出异常后执行;
@Around:环绕目标方法执行(可控制目标方法的执行与否,如事务的开始和提交);
切入点(Pointcut) :定义切面作用的目标方法(如'所有 Service 层的方法'),通过表达式(如 execution 表达式)指定;
连接点(JoinPoint) :目标方法的执行点(如方法调用、异常抛出),是切入点的具体实例;
织入(Weaving) :将切面逻辑融入目标方法的过程,Spring AOP 默认采用动态代理织入。
实现原理: Spring AOP 基于动态代理实现,分为两种代理方式:
JDK 动态代理 :
适用场景:目标类实现接口;
原理:通过 java.lang.reflect.Proxy 类动态生成代理类,代理类实现目标接口,并重写目标方法,在方法中织入切面逻辑;
缺点:仅支持接口代理,无法代理无接口的类。
CGLIB 动态代理 :
适用场景:目标类未实现接口;
原理:通过 CGLIB(Code Generation Library)字节码生成框架,动态生成目标类的子类,重写目标方法,织入切面逻辑;
优点:支持任意类代理(无需接口),性能优于 JDK 动态代理(创建代理类开销大,但执行效率高)。
应用场景:
日志记录 :记录方法调用参数、返回值、执行时间;
事务管理 :控制事务的开始、提交、回滚(Spring 声明式事务基于 AOP 实现);
权限校验 :方法执行前校验用户权限;
异常处理 :统一捕获目标方法的异常并处理。
示例:日志切面
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(public * com.example.service..*(..))")
public void servicePointcut () {}
@Around("servicePointcut()")
public Object logAround (ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("方法" + methodName + "调用,参数:" + Arrays.toString(args));
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long cost = System.currentTimeMillis() - start;
System.out.println("方法" + methodName + "返回值:" + result + ",执行时间:" + cost + "ms" );
return result;
}
}
3. Spring 事务管理的原理? Spring 事务管理核心是'声明式事务'(基于 AOP)和'编程式事务'(手动编码),其中声明式事务是主流用法。
1. 事务的 ACID 特性:
原子性(Atomicity) :事务是不可分割的最小单位,要么全部执行,要么全部回滚;
一致性(Consistency) :事务执行前后,数据完整性保持一致(如转账前后总金额不变);
隔离性(Isolation) :多个事务并发执行时,事务之间相互隔离,互不影响;
持久性(Durability) :事务提交后,数据修改永久生效(写入磁盘)。
2. 事务隔离级别(Spring 支持):
DEFAULT:默认隔离级别(依赖数据库,如 MySQL 默认 REPEATABLE READ);
READ_UNCOMMITTED:读未提交,最低隔离级别,可能出现脏读、不可重复读、幻读;
READ_COMMITTED:读已提交,避免脏读,可能出现不可重复读、幻读(如 Oracle 默认);
REPEATABLE_READ:可重复读,避免脏读、不可重复读,可能出现幻读(如 MySQL 默认);
SERIALIZABLE:串行化,最高隔离级别,避免所有并发问题,性能最低。
3. 事务传播行为(核心,解决事务嵌套问题):
REQUIRED(默认):如果当前存在事务,加入事务;如果没有事务,创建新事务;
REQUIRES_NEW:无论当前是否存在事务,都创建新事务(新事务与原事务相互独立,原事务暂停);
SUPPORTS:如果当前存在事务,加入事务;如果没有事务,以非事务方式执行;
NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,暂停原事务;
NEVER:以非事务方式执行,如果当前存在事务,抛出异常。
4. 声明式事务实现(基于 AOP):
配置方式 :通过 @EnableTransactionManagement 注解启用事务管理(Spring Boot 自动启用);
核心注解 :@Transactional(标注在类或方法上,类级别的注解对所有方法生效);
原理 :
@Transactional 注解被解析为切面,切入点是标注该注解的方法;
通知逻辑:通过 AOP 动态代理,在目标方法执行前开启事务,执行后提交事务,异常时回滚事务;
事务管理器:Spring 通过 PlatformTransactionManager 接口适配不同数据库(如 DataSourceTransactionManager 适配 JDBC,HibernateTransactionManager 适配 Hibernate)。
5. 事务失效的常见场景:
方法非 public 修饰(@Transactional 仅对 public 方法生效);
事务方法内部调用(如 A 方法调用本类的 B 方法,B 方法的 @Transactional 失效,因为未经过代理类);
异常类型不匹配(默认仅捕获 RuntimeException 和 Error,checked 异常需通过 rollbackFor 指定);
手动捕获异常未抛出(如 try-catch 异常但未 throw,事务无法感知异常,不会回滚);
传播行为配置错误(如 NOT_SUPPORTED、NEVER);
数据源未配置事务管理器(PlatformTransactionManager 未被 Spring 管理)。
六、数据库与 MyBatis
1. JDBC 的核心操作步骤? JDBC(Java Database Connectivity)是 Java 访问数据库的标准 API,核心步骤如下:
加载数据库驱动(JDK8+ 无需手动加载,DriverManager 自动扫描);
建立数据库连接(通过 DriverManager.getConnection(url, username, password));
创建 Statement/PreparedStatement 对象(执行 SQL 语句);
执行 SQL(executeQuery() 查询,executeUpdate() 增删改);
处理结果集(查询时通过 ResultSet 遍历结果);
关闭资源(ResultSet、Statement、Connection,需在 finally 中关闭,避免资源泄漏)。
public void queryUser () {
Connection conn = null ;
PreparedStatement pstmt = null ;
ResultSet rs = null ;
try {
Class.forName("com.mysql.cj.jdbc.Driver" );
String url = "jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC" ;
conn = DriverManager.getConnection(url, "root" , "123456" );
String sql = "SELECT id, name FROM user WHERE id = ?" ;
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1 , 1 );
rs = pstmt.executeQuery();
while (rs.next()) {
int id = rs.getInt("id" );
String name = rs.getString("name" );
System.out.println("id: " + id + ", name: " + name);
}
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
try {
if (rs != null ) rs.close();
if (pstmt != null ) pstmt.close();
if (conn != null ) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
PreparedStatement vs Statement :PreparedStatement 支持预编译 SQL、参数化查询,防止 SQL 注入,性能更优;Statement 不支持参数化,存在 SQL 注入风险;
SQL 注入 :如 SELECT * FROM user WHERE + name + "'",若 name 传入 ' OR '1'='1,则 SQL 变为 SELECT * FROM user WHERE OR '1'='1',查询所有用户;PreparedStatement 通过参数绑定避免该问题。
2. MyBatis 的核心组件与工作原理? MyBatis 是持久层框架,简化 JDBC 操作,通过 XML 或注解配置 SQL 语句,无需手动编写 JDBC 代码。
核心组件:
SqlSessionFactory :MyBatis 核心工厂类,通过 SqlSessionFactoryBuilder 读取配置文件(mybatis-config.xml)创建,线程安全;
SqlSession :会话对象,封装数据库连接和事务管理,线程不安全(每次请求创建新实例);
Mapper 接口 :自定义 DAO 接口,MyBatis 通过动态代理生成实现类,关联 XML/注解中的 SQL;
Mapper.xml :配置 SQL 语句、参数映射、结果集映射;
Configuration :MyBatis 全局配置对象,存储核心配置(如数据源、事务管理器、Mapper 注册)。
工作原理:
加载配置文件:SqlSessionFactoryBuilder 读取 mybatis-config.xml 和 Mapper.xml,解析配置信息(数据源、SQL 语句、映射规则);
创建 SqlSessionFactory:通过解析后的配置信息创建 SqlSessionFactory(单例模式);
创建 SqlSession:SqlSessionFactory 调用 openSession() 创建 SqlSession,默认不自动提交事务;
获取 Mapper 代理对象:SqlSession 调用 getMapper(Mapper 接口.class),通过动态代理生成 Mapper 接口的实现类;
执行 SQL:调用 Mapper 接口方法,MyBatis 根据方法名匹配 Mapper.xml 中的 SQL 语句,执行 JDBC 操作;
处理结果集:MyBatis 自动将 ResultSet 映射为 Java 对象(根据 resultType/resultMap 配置);
提交/回滚事务:执行完成后,SqlSession 调用 commit() 提交事务或 rollback() 回滚事务;
关闭 SqlSession:释放资源。
核心配置(mybatis-config.xml): <configuration >
<environments default ="development" >
<environment id ="development" >
<transactionManager type ="JDBC" />
<dataSource type ="POOLED" >
<property name ="driver" value ="com.mysql.cj.jdbc.Driver" />
<property name ="url" value ="jdbc:mysql://localhost:3306/test?useSSL=false" />
<property name ="username" value ="root" />
<property name ="password" value ="123456" />
</dataSource >
</environment >
</environments >
<mappers >
<mapper resource ="com/example/mapper/UserMapper.xml" />
</mappers >
</configuration >
Mapper.xml 示例: <mapper namespace ="com.example.mapper.UserMapper" >
<resultMap id ="UserResultMap" type ="com.example.entity.User" >
<id column ="id" property ="id" />
<result column ="name" property ="name" />
<result column ="age" property ="age" />
</resultMap >
<select id ="selectUserById" parameterType ="int" resultMap ="UserResultMap" >
SELECT id, name, age FROM user WHERE id = #{id}
</select >
<insert id ="insertUser" parameterType ="com.example.entity.User" >
INSERT INTO user (name, age) VALUES (#{name}, #{age})
</insert >
</mapper >
3. MyBatis 的一级缓存与二级缓存? MyBatis 提供缓存机制,减少数据库查询次数,提升性能,分为一级缓存和二级缓存。
一级缓存(SqlSession 级别,默认开启):
作用范围 :同一个 SqlSession 内,多次执行相同的 SQL 查询(参数相同),仅第一次查询数据库,后续从缓存获取结果;
实现原理 :SqlSession 内部维护一个 HashMap,key 为缓存键(由 SQL 语句、参数、RowBounds、环境等组成),value 为查询结果;
失效场景 :
执行 insert/update/delete 操作(会清空一级缓存);
调用 SqlSession.clearCache() 手动清空;
SqlSession 关闭或提交事务。
二级缓存(Mapper 级别,默认关闭):
作用范围 :同一个 Mapper 接口(命名空间)下,多个 SqlSession 共享缓存;
启用方式 :
在 mybatis-config.xml 中开启全局缓存(默认开启,可省略):<setting name="cacheEnabled" value="true"/>;
在 Mapper.xml 中添加 <cache/> 标签(启用当前 Mapper 的二级缓存);
实现原理 :每个 Mapper 接口对应一个 Cache 对象,SqlSession 查询后将结果存入二级缓存,其他 SqlSession 查询相同 SQL 时,先从二级缓存获取;
注意事项 :
缓存的对象必须实现 Serializable 接口(二级缓存可能序列化存储);
执行 insert/update/delete 操作会清空当前 Mapper 的二级缓存;
可通过 useCache="false"(查询语句)或 flushCache="true"(增删改语句)控制缓存行为。
缓存查询顺序 :二级缓存 → 一级缓存 → 数据库。
七、设计模式与性能优化
1. 单例模式的几种实现方式与线程安全? 单例模式确保一个类仅有一个实例,并提供全局访问点,常用实现方式如下:
1. 饿汉式(线程安全,非懒加载): public class Singleton {
private static final Singleton instance = new Singleton ();
private Singleton () {}
public static Singleton getInstance () {
return instance;
}
}
优点 :简单高效,类加载时初始化,天然线程安全;
缺点 :非懒加载,类加载时即创建实例,若实例未被使用,浪费内存。
2. 懒汉式(线程不安全→线程安全优化):
优点 :懒加载(使用时才创建实例),线程安全,性能优;
关键 :volatile 关键字必须加,防止 instance = new Singleton() 指令重排(分配内存→初始化→赋值),导致其他线程获取到未初始化的实例。
public class Singleton {
private static volatile Singleton instance;
private Singleton () {}
public static Singleton getInstance () {
if (instance == null ) {
synchronized (Singleton.class) {
if (instance == null ) {
instance = new Singleton ();
}
}
}
return instance;
}
}
public class Singleton {
private static Singleton instance;
private Singleton () {}
public static Singleton getInstance () {
if (instance == null ) {
instance = new Singleton ();
}
return instance;
}
}
3. 静态内部类式(线程安全,懒加载,推荐): public class Singleton {
private Singleton () {}
private static class SingletonHolder {
private static final Singleton instance = new Singleton ();
}
public static Singleton getInstance () {
return SingletonHolder.instance;
}
}
优点 :懒加载(静态内部类按需加载),线程安全(类加载机制保证),代码简洁,无锁开销;
原理 :静态内部类的加载时机是在第一次被引用时,类加载过程是线程安全的,确保实例仅创建一次。
4. 枚举式(线程安全,防反射/序列化,最佳实践): public enum Singleton {
INSTANCE;
public void doSomething () {
System.out.println("Singleton enum" );
}
}
优点 :
线程安全:枚举类的实例在类加载时创建,天然线程安全;
防反射:枚举类的构造方法被编译器私有化,无法通过反射创建实例;
防序列化:枚举类默认实现 Serializable,序列化时不会创建新实例;
缺点 :非懒加载,类加载时即创建实例。
2. Java 性能优化的常见手段? Java 性能优化需从'代码层面、JVM 层面、数据库层面、架构层面'多维度入手,核心目标是提升响应速度、降低资源消耗。
1. 代码层面优化:
集合使用优化 :
ArrayList 初始化时指定初始容量(避免频繁扩容);
频繁增删用 LinkedList,频繁查询用 ArrayList;
避免在循环中使用 ArrayList.add(index, element)(O(n) 复杂度);
字符串优化 :
频繁拼接用 StringBuilder(单线程)/StringBuffer(多线程),避免 String 拼接(创建大量临时对象);
常量字符串用 String.intern() 复用常量池对象;
循环优化 :
减少循环内的对象创建(如 for (int i = 0; i < list.size(); i++) → 先获取 size:int size = list.size(); for (int i = 0; i < size; i++));
避免循环内的复杂计算(如方法调用、表达式计算);
避免空指针 :使用 Objects.requireNonNull()、Optional 类,减少 null 判断;
资源管理 :使用 try-with-resources 自动关闭流、数据库连接,避免资源泄漏。
2. JVM 层面优化:
堆内存配置 :合理设置 -Xms(初始堆)和 -Xmx(最大堆),建议两者相等(避免频繁扩容),堆大小一般为物理内存的 1/2~1/3;
年轻代优化 :调整 -XX:NewRatio(年轻代与老年代比例,默认 2:1)、-XX:SurvivorRatio(Eden 与 S0/S1 比例,默认 8:1),根据应用对象存活时间调整;
GC 收集器选择 :
高吞吐量场景:使用 Parallel Scavenge 收集器;
低延迟场景:使用 G1/ZGC 收集器;
逃逸分析 :启用 -XX:+DoEscapeAnalysis(默认启用),JVM 自动分析对象是否逃逸,未逃逸的对象可分配在栈上(减少 GC 压力)。
3. 数据库层面优化:
索引优化 :给查询频繁的字段建立索引(如 WHERE、JOIN、ORDER BY 字段),避免过度索引(影响增删效率);
SQL 优化 :
避免 SELECT *,只查询需要的字段;
避免 WHERE 子句中使用函数(如 WHERE DATE(create_time) = '2025-01-01',导致索引失效);
避免 JOIN 过多表(建议不超过 3 张表),大表 JOIN 用分页;
批量操作替代循环单条操作(如 MyBatis 的 batchInsert);
连接池优化 :合理设置数据库连接池大小(如 HikariCP 的 maximum-pool-size,建议为 CPU 核心数×2+1),避免连接泄漏。
4. 架构层面优化:
缓存引入 :使用 Redis、Ehcache 等缓存热点数据(如用户信息、配置数据),减少数据库查询;
异步处理 :将耗时操作(如日志打印、邮件发送)异步化(使用线程池、消息队列),提升响应速度;
负载均衡 :通过 Nginx、LVS 等实现多服务器负载均衡,分散请求压力;
分布式部署 :将应用拆分为微服务,按业务模块部署,提高并发处理能力。
八、总结 本文覆盖 Java 面试核心考点,从基础语法、集合框架、多线程、JVM、Spring、数据库到设计模式与性能优化,每个模块均包含高频面试题、详细答案及核心考点解析,兼顾理论深度与实战应用。
面试时,除了记忆答案,更要理解底层原理(如 HashMap 的哈希冲突解决、Spring AOP 的动态代理、JVM 的 GC 机制),并结合项目经验说明实际应用场景(如线程池在项目中的配置、事务失效的排查过程)。建议重点掌握多线程并发、JVM、Spring 核心原理等高级考点,这些是区分初级与中高级开发者的关键。
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online