分为以下几个核心模块,每个模块都包含核心考点、典型题目、深度剖析和优秀回答范例。

Java 基础
这是面试的基石,主要考察候选人对 Java 语言本身的理解深度。
equals() 和 hashCode()
-
核心考点:
Object类中equals()和hashCode()的默认实现。- 重写
equals()的约定(自反性、对称性、传递性、一致性、非空性)。 hashCode()的约定(一致性、相同对象必须相同哈希码、不同对象可以相同哈希码)。- 为什么重写
equals()必须重写hashCode()?(HashMap 的原理)
-
典型题目:
- 和
equals()的区别是什么? - 什么情况下需要重写
equals()和hashCode()? - 详细解释一下为什么在重写
equals()的同时,必须重写hashCode()? - 手写一个
equals()和hashCode()方法。
- 和
-
深度剖析:
(图片来源网络,侵删)equals()vs :- 对于基本数据类型,比较的是值是否相等;对于引用数据类型,比较的是内存地址(引用)是否相等。
equals():Object类中的默认实现和 一样,但很多类(如String,Integer)都重写了它,用来比较是否相等,这是面试官考察的重点,需要分情况讨论。
hashCode()的契约:- 如果两个对象根据
equals()方法是相等的,那么调用这两个对象中任意一个对象的hashCode()方法都必须产生相同的整数结果。 - 如果两个对象根据
equals()方法是不相等的,那么调用这两个对象中任意一个对象的hashCode()方法不一定需要产生不同的整数结果(但不同对象产生不同哈希码能提高哈希表性能)。
- 如果两个对象根据
- 为什么必须一起重写?
- 核心原因:为了
HashMap、HashSet、Hashtable等哈希集合的正确工作。 HashMap的工作原理:通过key的hashCode()找到在哈希表(数组)中的位置(桶),然后在该位置通过equals()方法比较key是否真正相等,以确定是覆盖还是新增。- 反例:如果只重写了
equals()而没有重写hashCode(),那么两个内容相同的对象,它们的hashCode()可能是不同的(Object默认基于内存地址),当你把第一个对象put进HashMap后,再用第二个(内容相同但hashCode不同)的get时,HashMap会去一个错误的桶里查找,导致找不到,返回null,这破坏了集合的约定。
- 核心原因:为了
-
优秀回答范例 (以“为什么必须一起重写?”为例):
“这是一个非常经典且重要的问题。为了保证基于哈希码的集合(如
HashMap、HashSet)能够正常工作,我们必须同时重写equals()和hashCode()。-
HashMap的基本原理:HashMap内部使用一个数组(哈希桶)来存储数据,当调用put(key, value)时,它会先计算key的hashCode(),通过这个哈希码找到数组中的索引位置(桶),它会在这个桶里使用equals()方法来比较新key和桶中已有的key是否真正相等,如果相等,则覆盖旧值;如果不相等,则作为新的条目添加到链表或红黑树中。 -
契约的破坏:
Object类对hashCode()的契约是:a.equals(b)为true,a.hashCode()必须等于b.hashCode()。
(图片来源网络,侵删) -
反证法:假设我们有一个
Person类,我们根据name和age判断两个对象是否相等,所以重写了equals()方法,但忘记重写hashCode()。Person p1 = new Person("Alice", 30); Person p2 = new Person("Alice", 30); System.out.println(p1.equals(p2)); // 输出 true System.out.println(p1.hashCode() == p2.hashCode()); // 输出 false (因为Object默认实现基于地址)我们将
p1放入一个HashMap:Map<Person, String> map = new HashMap<>(); map.put(p1, "P1's Info"); System.out.println(map.get(p2)); // 我们期望得到 "P1's Info",但实际会得到 null
为什么会这样? 因为
map.get(p2)时,HashMap先计算p2.hashCode(),得到一个哈希码,然后去对应的桶里查找,由于p2.hashCode()和p1.hashCode()不同,它去了一个错误的桶里找,自然找不到p1,所以返回null,这就导致了逻辑上的严重错误。 -
重写
equals()确立了对象的“内容相等”标准,而重写hashCode()则确保了这种“内容相等”的对象在哈希集合中能被正确地定位和识别,二者缺一不可,共同构成了 Java 对象相等性的完整契约。”
-
集合框架
Java 集合是面试的重中之重,考察候选人对数据结构和算法的理解。
HashMap 的工作原理
-
核心考点:
HashMap的底层数据结构(JDK 1.7 vs 1.8)。put()方法的完整流程(哈希计算、索引定位、冲突处理)。hash()方法的扰动函数(h ^ (h >>> 16))的作用。- 扩容机制(
resize())。 - 为什么
HashMap的容量是 2 的幂次方? HashMap和Hashtable的区别。HashMap在多线程环境下的不安全性(死循环、数据丢失)。
-
典型题目:
- 详细描述一下
HashMap的put过程。 HashMap是如何解决哈希冲突的?- 为什么
HashMap的初始容量是 16,并且扩容必须是 2 倍? HashMap和ConcurrentHashMap有什么区别?ConcurrentHashMap是如何保证线程安全的?
- 详细描述一下
-
深度剖析:
- 数据结构演变:
- JDK 1.7:数组 + 链表,冲突时,新元素直接插入到链表头部(头插法)。
- JDK 1.8:数组 + (链表 / 红黑树),当链表长度超过 8 (
TREEIFY_THRESHOLD) 且数组长度超过 64 时,链表会转换为红黑树,以解决链表过长导致的查询性能下降(O(n) -> O(log n)),当红黑树节点数小于 6 时,会退化为链表。插入方式也改为了尾插法,避免了在多线程扩容时可能导致死循环的问题。
- 扰动函数:
hash = (h = key.hashCode()) ^ (h >>> 16),它的作用是让哈希值的分布更均匀,因为hashCode()返回的是一个 32 位整数,而HashMap的初始容量是 16,取模运算(hash & (capacity - 1))只依赖于低位,通过将高 16 位与低 16 位异或,可以引入高位的信息,使得最终计算出的索引分布更散列,减少冲突。 - 容量是 2 的幂次方:
- 高效取模:
hash & (capacity - 1)等价于hash % capacity,但位运算的效率远高于取模运算。 - 均匀分布:
capacity - 1的二进制形式全是 1(如 15 是1111),这样hash值的每一位都能参与到索引的计算中,保证了索引的均匀分布,避免了不必要的哈希冲突。
- 高效取模:
- 多线程问题:
- 死循环 (JDK 1.7):在多线程扩容时,如果两个线程同时发现
HashMap需要扩容,它们会同时进行resize操作,由于使用头插法,可能会导致链表的指针指向错误,形成环形链表,下次get操作时就会陷入死循环。 - 数据丢失:两个线程同时
put操作,可能计算出的索引位置相同,在赋值table[i] = newNode时,后一个线程会覆盖前一个线程的赋值,导致数据丢失。
- 死循环 (JDK 1.7):在多线程扩容时,如果两个线程同时发现
- 数据结构演变:
-
优秀回答范例 (以“
put过程”为例):“好的,我来详细描述一下 JDK 1.8 中
HashMap的put方法流程:-
参数校验:首先检查
key和value是否为null。HashMap允许key为null,其哈希值固定为 0,会存储在数组的第一个位置(索引 0)。 -
计算哈希值:调用
key.hashCode()得到原始哈希值,然后通过hash()方法(扰动函数)计算出最终的哈希码。 -
定位索引:根据计算出的哈希码和当前数组的长度,通过
(n - 1) & hash计算出该元素在数组中的索引位置i。 -
遍历桶(数组元素):
- 该位置为空:
tab[i]是null,直接创建一个新节点Node放入该位置即可。 - 该位置不为空(发生哈希冲突):
a. 检查是否是同一个
key:首先比较tab[i]的hash值和key的hash值是否相等,然后调用key.equals()方法比较内容,如果相等,说明是覆盖操作,用新的value覆盖旧的value,并返回旧value。 b. 检查是否是树节点:tab[i]是TreeNode类型(说明该位置已经是红黑树),则调用putTreeVal()方法将新节点插入红黑树中,并平衡树结构。 c. 遍历链表:tab[i]是普通Node类型(链表),则遍历这个链表,在遍历过程中,同样进行hash和equals比较,如果找到相同的key,则覆盖并返回,如果遍历到链表末尾都没有找到,则将新节点添加到链表末尾(尾插法)。
- 该位置为空:
-
检查链表是否需要树化:在将新节点添加到链表后,会检查
链表长度是否 >= 8数组总长度是否 >= 64,如果两个条件都满足,则会将该链表转换为红黑树,否则,如果数组长度小于 64,则优先进行扩容。 -
检查是否需要扩容:在每次添加元素后,都会检查
size是否超过了threshold(阈值 = capacity * load factor),如果超过了,则调用resize()方法进行扩容,扩容会创建一个新数组,大小是原数组的 2 倍,并重新计算所有元素的在新数组中的位置,这个过程叫 rehash。”
-
并发编程
这是区分中高级工程师的关键,考察候选人对多线程问题的理解和解决能力。
volatile 关键字
-
核心考点:
volatile的两大特性:保证可见性 和 禁止指令重排序。volatile和synchronized的区别。volatile的使用场景(状态标记量、单例模式的双重检查锁)。
-
典型题目:
volatile关键字的作用是什么?能保证原子性吗?volatile和synchronized的区别?- 为什么双重检查锁实现的单例模式要用
volatile修饰instance?
-
深度剖析:
-
保证可见性:当一个线程修改了被
volatile修饰的变量时,JMM(Java 内存模型)会立刻将该变量的修改值从工作内存同步回主内存,并且其他线程在读取这个变量时,会直接从主内存读取,保证了线程间变量的可见性,普通变量则可能因为每个线程有自己的工作内存缓存而导致不可见。 -
禁止指令重排序:
volatile关键字会插入一个“内存屏障”(Memory Barrier),内存屏障可以禁止其前后的指令进行重排序优化,保证了程序的执行顺序。 -
不保证原子性:
volatile只能保证单个读/写操作的原子性,但不能保证复合操作的原子性。i++,它包含“读取-修改-写入”三个步骤,volatile无法保证这三个步骤不被其他线程打断。 -
双重检查锁的
volatile:// 问题代码 if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); // 问题所在 } } }instance = new Singleton()这行代码并非原子操作,大致可以分解为三步:memory = allocate();// 分配对象内存ctorInstance(memory);// 初始化对象instance = memory;// 建立instance引用指向分配的内存地址
由于指令重排序,JVM 可能会先执行 1 和 3,再执行 2,另一个线程在第一次检查
if (instance == null)时,发现instance已经不为null了(因为它指向了分配的内存,但对象还未初始化),就会返回一个未初始化完成的instance对象,导致程序出错。volatile的作用:volatile会禁止 2 和 3 的重排序,保证了instance引用在指向内存地址时,对象一定已经被初始化完毕了。
-
-
优秀回答范例 (以“
volatile的作用”为例):“
volatile是 Java 并发编程中一个轻量级的同步机制,它主要有两大核心作用:-
保证变量的可见性:
- 问题背景:在 Java 内存模型中,每个线程都有自己的工作内存,线程对变量的操作都在工作内存中进行,然后同步回主内存,这导致了“可见性”问题:一个线程修改了变量,其他线程可能看不到最新的值。
volatile的解决方案:当一个变量被volatile修饰后:- 写操作:当线程修改
volatile变量时,JMM 会强制将该线程工作内存中的值立刻刷新到主内存。 - 读操作:当线程读取
volatile变量时,JMM 会强制让线程从主内存中读取最新值,而不是使用工作内存中的缓存副本。
- 写操作:当线程修改
volatile确保了线程间对变量的修改是立即可见的。
-
禁止指令重排序:
- 问题背景:为了优化性能,编译器和处理器可能会对指令进行重排序,但在并发场景下,重排序可能会破坏代码的逻辑。
volatile的解决方案:volatile关键字会插入一个“内存屏障”,内存屏障可以禁止其前后的指令进行重排序优化,保证了程序的执行顺序严格按照代码的顺序来。- 经典应用:在双重检查锁实现的单例模式中,
volatile防止了new Singleton()操作(分配内存、初始化、建立引用)的重排序,避免了其他线程拿到一个未初始化完成的实例。
volatile的局限性:- 不保证原子性:
volatile只能保证单个读/写操作的原子性,像i++这样的复合操作,volatile无法保证其原子性,必须使用synchronized或java.util.concurrent.atomic包下的原子类。
与
synchronized的区别:synchronized是一个锁机制,它不仅能保证可见性,还能保证原子性,同时会阻塞线程,是重量级的。volatile只是一个关键字,它通过内存屏障来保证可见性和有序性,不会阻塞线程,是轻量级的。synchronized可以修饰代码块和方法,而volatile只能修饰变量。”
-
JVM (Java 虚拟机)
JVM 是 Java 程序的运行基石,理解 JVM 能写出更高性能、更稳定的代码。
Java 内存模型与运行时数据区
-
核心考点:
- 运行时数据区:程序计数器、Java 虚拟机栈、本地方法栈、堆、方法区。
- 各区域的作用、是否线程共享、是否会发生
OutOfMemoryError和StackOverflowError。 - 垃圾回收:GC Roots、可达性分析算法、常见的垃圾回收器(Serial, Parallel, CMS, G1, ZGC)。
- 类加载机制:加载、验证、准备、解析、初始化。
-
典型题目:
- 画一下 Java 内存模型(运行时数据区)。
- 什么情况下会触发
OutOfMemoryError?什么情况下会触发StackOverflowError? - 简述一下垃圾回收的算法和常见的垃圾回收器。
- 讲一下类加载的双亲委派模型。
-
深度剖析:
- 运行时数据区:
- 程序计数器:记录当前线程执行的字节码行号,是唯一一个不会在
OutOfMemoryError的区域。 - 虚拟机栈 & 本地方法栈:存储栈帧(局部变量表、操作数栈、动态链接、方法出口),栈深度过大(如无限递归)会导致
StackOverflowError,栈动态扩展失败会导致OutOfMemoryError。 - 堆:存放对象实例和数组,是垃圾收集的主要区域,内存不足会抛出
OutOfMemoryError。 - 方法区:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等,JDK 8 以后用元空间代替,元空间在本地内存中,
OutOfMemoryError的表现不同。
- 程序计数器:记录当前线程执行的字节码行号,是唯一一个不会在
- 垃圾回收:
- GC Roots:一组必须存活的对象,可以作为起点,通过可达性分析算法判断对象是否存活,虚拟机栈中引用的对象、方法区中类静态属性引用的对象、本地方法栈中 JNI (即 Native 方法) 引用的对象等。
- 垃圾回收器:
- Serial / Serial Old:单线程,进行垃圾回收时必须暂停用户线程(Stop-The-World),适用于客户端模式。
- Parallel Scavenge / Parallel Old:Serial 的多线程版本,能充分利用多核 CPU,是吞吐量优先的收集器。
- CMS (Concurrent Mark Sweep):以获取最短回收停顿时间为目标的收集器,基于“标记-清除”算法,标记和清除过程可以和用户线程并发,但初始标记和重新标记需要 STW,缺点是会产生内存碎片和并发模式失败。
- G1 (Garbage-First):面向服务端的垃圾回收器,将堆划分为多个大小相等的 Region,跟踪每个 Region 的回收价值,在有限时间内优先回收价值最大的 Region(即“垃圾优先”),能建立可预测的停顿时间模型。
- ZGC / Shenandoah:追求极致低停顿时间的垃圾回收器,采用着色指针等技术,几乎在整个生命周期中都可以并发执行,STW 时间极短(毫秒甚至微秒级)。
- 双亲委派模型:
- 工作流程:当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己去加载。
- 核心作用:
- 安全性:防止核心 API 被篡改,如果有人自己写了一个
java.lang.String类,双亲委派模型会保证加载的是rt.jar中的核心String类,而不是用户自定义的,保证了 Java 运行的安全稳定。 - 避免重复加载:如果同一个类被多个类加载器加载,会存在于不同的命名空间中,但双亲委派模型保证了类只会在一个加载器中被加载,避免了内存浪费。
- 安全性:防止核心 API 被篡改,如果有人自己写了一个
- 运行时数据区:
-
优秀回答范例 (以“JVM 内存模型”为例):
“Java 虚拟机的运行时数据区是 Java 程序运行时的内存划分,主要包括以下几个部分:
-
程序计数器:
- 作用:是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器,它记录了下一条要执行的 JVM 指令的地址。
- 特点:是线程私有的,每个线程都有自己独立的计数器,它的生命周期与线程相同。唯一一个在 JVM 规范中没有规定任何
OutOfMemoryError情况的区域。
-
Java 虚拟机栈:
- 作用:描述的是 Java 方法执行的内存模型,每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
- 特点:是线程私有的,生命周期与线程相同。
- 异常:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出
StackOverflowError。 - 如果虚拟机栈可以动态扩展(大部分 JVM 可扩展),但在扩展时无法申请到足够的内存,会抛出
OutOfMemoryError。
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出
-
本地方法栈:
- 作用:与虚拟机栈类似,但它为虚拟机使用到的
Native方法(非 Java 语言实现的方法)服务。 - 特点:也是线程私有的。
- 异常:同样会抛出
StackOverflowError和OutOfMemoryError。
- 作用:与虚拟机栈类似,但它为虚拟机使用到的
-
Java 堆:
- 作用:是 Java 内存管理中最大的一块,被所有线程共享,它唯一的目的是存放对象实例和数组,几乎所有对象的实例都在这里分配内存。
- 特点:是垃圾收集器管理的主要区域,因此也被称为“GC 堆”。
- 异常:如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出
OutOfMemoryError。
-
方法区:
- 作用:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据,它逻辑上是堆的一部分,但为了与 Java 堆区分,有一个别名叫做 Non-Heap(非堆)。
- 特点:是线程共享的。
- 演变:在 JDK 1.7 及之前,称为“永久代”(PermGen),在 JDK 8 及之后,被元空间取代,元空间直接使用本地内存,不在虚拟机内存中,所以元空间的内存大小只受本地内存限制。
- 异常:如果方法区无法满足新的内存分配需求,会抛出
OutOfMemoryError。
栈和程序计数器是线程私有的,负责方法的执行;堆和方法区是线程共享的,负责数据的存储,理解这个划分对于排查内存溢出、分析性能瓶颈至关重要。”
-
Spring / Spring Boot
作为 Java 开发的事实标准,Spring 相关的知识是必考项。
IoC (控制反转) / DI (依赖注入)
-
核心考点:
- IoC 和 DI 的概念与关系。
- Bean 的生命周期。
- Bean 的作用域(
singleton,prototype等)。 - 自动装配的原理和方式。
@Autowired和@Resource的区别。
-
典型题目:
- 解释一下什么是 IoC 和 DI?
- Spring 是如何管理 Bean 的?Bean 的生命周期是怎样的?
@Autowired是如何实现自动注入的?默认是按类型还是按名称?- 说说你对 Spring Bean 的作用域的理解。
-
深度剖析:
- IoC vs DI:
- IoC (Inversion of Control):是一种思想,一种设计原则,它的核心思想是将原本在程序中手动创建对象、管理对象之间依赖的权利,反转给容器来控制,以前是
new A(),现在是A a = container.getBean("a")。 - DI (Dependency Injection):是实现 IoC 思想的一种具体方式,它指的是容器在创建 Bean 时,自动将 Bean 所依赖的其他实例(依赖)注入到 Bean 的内部。
ServiceA依赖RepositoryB,DI 就是让 Spring 把RepositoryB的实例注入到ServiceA中。 - 关系:DI 是实现 IoC 最常见、最直接的方式,我们通常所说的 Spring IoC 容器,其本质就是一个实现了依赖注入功能的容器。
- IoC (Inversion of Control):是一种思想,一种设计原则,它的核心思想是将原本在程序中手动创建对象、管理对象之间依赖的权利,反转给容器来控制,以前是
- Bean 的生命周期:
- 实例化:通过反射创建 Bean 对象。
- 填充属性:为 Bean 的属性(依赖)进行赋值(注入)。
- 初始化:
- 调用
BeanNameAware的setBeanName()。 - 调用
BeanFactoryAware的setBeanFactory()。 - 调用
ApplicationContextAware的setApplicationContext()。 - 调用
BeanPostProcessor的postProcessBeforeInitialization()。 - 调用 Bean 自身的
@PostConstruct方法和init-method配置的方法。 - 调用
BeanPostProcessor的postProcessAfterInitialization()。
- 调用
- 使用:Bean 准备就绪,可以被应用使用了。
- 销毁:
- 调用
@PreDestroy方法和destroy-method配置的方法。 - 容器关闭时,Bean 被销毁。
- 调用
@Autowiredvs@Resource:- 来源不同:
@Autowired是 Spring 框架的注解;@Resource是 JSR-250 规范的注解(JDK 自带)。 - 注入方式不同:
@Autowired:默认按类型进行注入,如果找到多个同类型的 Bean,会再按名称(@Qualifier)或属性名进行匹配,可以配合@Qualifier精确指定 Bean 的名称。@Resource:默认按名称进行注入,它会先在容器中查找与属性名相同的 Bean,找不到再按类型查找,可以通过name属性显式指定 Bean 的名称。
- 支持位置不同:
@Autowired可以用于构造器、方法、字段、参数;@Resource主要用于字段和 setter 方法。
- 来源不同:
- IoC vs DI:
-
优秀回答范例 (以“IoC 和 DI”为例):
“好的,这是一个非常核心的概念,我来详细解释一下。
IoC (Inversion of Control) - 控制反转
- 本质:它是一种设计思想,而不是一个具体的技术,它的核心思想是反转程序的控制权。
- 对比:
- 传统方式(正转):我们在代码中主动创建和管理对象及其依赖,在
ServiceA类中,我们需要RepositoryB,就直接new RepositoryB()。ServiceA完全控制了RepositoryB的创建和生命周期。 - IoC 方式(反转):我们将创建和管理对象(包括它们的依赖关系)的控制权,交给了外部容器(Spring IoC 容器)。
ServiceA不再关心RepositoryB是如何创建的,甚至不直接new它,它只需要声明“我需要一个RepositoryB”,容器就会在合适的时机,把一个已经创建好的RepositoryB实例交给ServiceA。
- 传统方式(正转):我们在代码中主动创建和管理对象及其依赖,在
- IoC 的好处是解耦,对象之间不再有硬编码的依赖,使得代码更灵活、更容易测试和维护。
DI (Dependency Injection) - 依赖注入
- 本质:它是实现 IoC 思想的一种具体方式,如果说 IoC 是一个宏观的“目标”,DI 就是达成这个目标的“手段”。
- 实现方式:DI 是指由容器在运行期间,动态地将依赖对象注入到需要它的组件中。
- 对比:
- 在传统方式中,
ServiceA通过new关键字主动“拉取”(Pull)了RepositoryB的依赖。 - 在 DI 方式中,
ServiceA声明它需要一个RepositoryB,然后容器主动“推送”(Push)一个RepositoryB的实例给ServiceA。
- 在传统方式中,
- 注入方式:Spring 主要支持三种注入方式:
- 构造器注入:通过类的构造函数注入依赖,推荐使用,因为依赖在对象创建时就已确定,保证了对象的不可变性。
- Setter 注入:通过 setter 方法注入依赖,可选依赖更灵活。
- 字段注入:直接在字段上使用
@Autowired注解,虽然简洁,但不利于单元测试和依赖管理,不推荐。
两者的关系
- IoC 是思想,DI 是实现,我们通常说的 Spring IoC 容器,其核心功能就是实现了依赖注入,DI 是目前最主流、最成熟的 IoC 实现方式。
- 一个形象的比喻:IoC 就像一个餐厅的老板,他决定菜单上有什么菜(Bean),而 DI 就像服务员,根据你的点单(依赖声明),把菜(依赖对象)端到你桌上,老板(IoC)控制了整个厨房(对象创建),服务员(DI)负责上菜(注入依赖)。”
总结与建议
- 基础为王:
equals/hashCode、HashMap、volatile、JVM 内存模型这些基础概念,一定要理解透彻,能够用自己的话清晰地讲出来。 - 知其然,更要知其所以然:不要只背答案,不要只背
HashMap的put流程,要理解为什么要有扰动函数、为什么容量要是 2 的幂次方、JDK 1.8 为什么要改成红黑树和尾插法。 - 框架原理:对于 Spring、MyBatis 等框架,要理解其核心设计思想和实现原理,而不仅仅是会用
@Autowired。 - 项目经验:将理论知识与你的实际项目经验结合起来,在项目中遇到了什么并发问题,你是如何用
volatile或synchronized解决的?项目中的性能瓶颈是什么,你是如何通过分析 JVM 和 GC 来优化的? - 沟通与表达:面试不仅是考察知识,更是考察沟通能力,回答问题时,逻辑要清晰,语言要流畅,最好能使用“总-分-总”的结构,先给出结论,再分点阐述,最后总结。
祝你面试顺利!
