杰瑞科技汇

Java面试题如何深度剖析?核心考点有哪些?

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

Java面试题如何深度剖析?核心考点有哪些?-图1
(图片来源网络,侵删)

Java 基础

这是面试的基石,主要考察候选人对 Java 语言本身的理解深度。

equals()hashCode()

  • 核心考点

    • Object 类中 equals()hashCode() 的默认实现。
    • 重写 equals() 的约定(自反性、对称性、传递性、一致性、非空性)。
    • hashCode() 的约定(一致性、相同对象必须相同哈希码、不同对象可以相同哈希码)。
    • 为什么重写 equals() 必须重写 hashCode()?(HashMap 的原理)
  • 典型题目

    1. equals() 的区别是什么?
    2. 什么情况下需要重写 equals()hashCode()
    3. 详细解释一下为什么在重写 equals() 的同时,必须重写 hashCode()
    4. 手写一个 equals()hashCode() 方法。
  • 深度剖析

    Java面试题如何深度剖析?核心考点有哪些?-图2
    (图片来源网络,侵删)
    • equals() vs
      • 对于基本数据类型,比较的是是否相等;对于引用数据类型,比较的是内存地址(引用)是否相等。
      • equals()Object 类中的默认实现和 一样,但很多类(如 String, Integer)都重写了它,用来比较是否相等,这是面试官考察的重点,需要分情况讨论。
    • hashCode() 的契约
      • 如果两个对象根据 equals() 方法是相等的,那么调用这两个对象中任意一个对象的 hashCode() 方法都必须产生相同的整数结果。
      • 如果两个对象根据 equals() 方法是不相等的,那么调用这两个对象中任意一个对象的 hashCode() 方法一定需要产生不同的整数结果(但不同对象产生不同哈希码能提高哈希表性能)。
    • 为什么必须一起重写?
      • 核心原因:为了 HashMapHashSetHashtable 等哈希集合的正确工作。
      • HashMap 的工作原理:通过 keyhashCode() 找到在哈希表(数组)中的位置(桶),然后在该位置通过 equals() 方法比较 key 是否真正相等,以确定是覆盖还是新增。
      • 反例:如果只重写了 equals() 而没有重写 hashCode(),那么两个内容相同的对象,它们的 hashCode() 可能是不同的(Object 默认基于内存地址),当你把第一个对象 putHashMap 后,再用第二个(内容相同但 hashCode 不同)的 get 时,HashMap 会去一个错误的桶里查找,导致找不到,返回 null,这破坏了集合的约定。
  • 优秀回答范例 (以“为什么必须一起重写?”为例)

    “这是一个非常经典且重要的问题。为了保证基于哈希码的集合(如 HashMapHashSet)能够正常工作,我们必须同时重写 equals()hashCode()

    1. HashMap 的基本原理HashMap 内部使用一个数组(哈希桶)来存储数据,当调用 put(key, value) 时,它会先计算 keyhashCode(),通过这个哈希码找到数组中的索引位置(桶),它会在这个桶里使用 equals() 方法来比较新 key 和桶中已有的 key 是否真正相等,如果相等,则覆盖旧值;如果不相等,则作为新的条目添加到链表或红黑树中。

    2. 契约的破坏Object 类对 hashCode() 的契约是:a.equals(b)truea.hashCode() 必须等于 b.hashCode()

      Java面试题如何深度剖析?核心考点有哪些?-图3
      (图片来源网络,侵删)
    3. 反证法:假设我们有一个 Person 类,我们根据 nameage 判断两个对象是否相等,所以重写了 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,这就导致了逻辑上的严重错误。

    4. 重写 equals() 确立了对象的“内容相等”标准,而重写 hashCode() 则确保了这种“内容相等”的对象在哈希集合中能被正确地定位和识别,二者缺一不可,共同构成了 Java 对象相等性的完整契约。”


集合框架

Java 集合是面试的重中之重,考察候选人对数据结构和算法的理解。

HashMap 的工作原理

  • 核心考点

    • HashMap 的底层数据结构(JDK 1.7 vs 1.8)。
    • put() 方法的完整流程(哈希计算、索引定位、冲突处理)。
    • hash() 方法的扰动函数(h ^ (h >>> 16))的作用。
    • 扩容机制(resize())。
    • 为什么 HashMap 的容量是 2 的幂次方?
    • HashMapHashtable 的区别。
    • HashMap 在多线程环境下的不安全性(死循环、数据丢失)。
  • 典型题目

    1. 详细描述一下 HashMapput 过程。
    2. HashMap 是如何解决哈希冲突的?
    3. 为什么 HashMap 的初始容量是 16,并且扩容必须是 2 倍?
    4. HashMapConcurrentHashMap 有什么区别?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 的幂次方
      1. 高效取模hash & (capacity - 1) 等价于 hash % capacity,但位运算的效率远高于取模运算。
      2. 均匀分布capacity - 1 的二进制形式全是 1(如 15 是 1111),这样 hash 值的每一位都能参与到索引的计算中,保证了索引的均匀分布,避免了不必要的哈希冲突。
    • 多线程问题
      • 死循环 (JDK 1.7):在多线程扩容时,如果两个线程同时发现 HashMap 需要扩容,它们会同时进行 resize 操作,由于使用头插法,可能会导致链表的指针指向错误,形成环形链表,下次 get 操作时就会陷入死循环。
      • 数据丢失:两个线程同时 put 操作,可能计算出的索引位置相同,在赋值 table[i] = newNode 时,后一个线程会覆盖前一个线程的赋值,导致数据丢失。
  • 优秀回答范例 (以“put 过程”为例)

    “好的,我来详细描述一下 JDK 1.8 中 HashMapput 方法流程:

    1. 参数校验:首先检查 keyvalue 是否为 nullHashMap 允许 keynull,其哈希值固定为 0,会存储在数组的第一个位置(索引 0)。

    2. 计算哈希值:调用 key.hashCode() 得到原始哈希值,然后通过 hash() 方法(扰动函数)计算出最终的哈希码。

    3. 定位索引:根据计算出的哈希码和当前数组的长度,通过 (n - 1) & hash 计算出该元素在数组中的索引位置 i

    4. 遍历桶(数组元素)

      • 该位置为空tab[i]null,直接创建一个新节点 Node 放入该位置即可。
      • 该位置不为空(发生哈希冲突): a. 检查是否是同一个 key:首先比较 tab[i]hash 值和 keyhash 值是否相等,然后调用 key.equals() 方法比较内容,如果相等,说明是覆盖操作,用新的 value 覆盖旧的 value,并返回旧 value。 b. 检查是否是树节点tab[i]TreeNode 类型(说明该位置已经是红黑树),则调用 putTreeVal() 方法将新节点插入红黑树中,并平衡树结构。 c. 遍历链表tab[i] 是普通 Node 类型(链表),则遍历这个链表,在遍历过程中,同样进行 hashequals 比较,如果找到相同的 key,则覆盖并返回,如果遍历到链表末尾都没有找到,则将新节点添加到链表末尾(尾插法)。
    5. 检查链表是否需要树化:在将新节点添加到链表后,会检查 链表长度是否 >= 8 数组总长度是否 >= 64,如果两个条件都满足,则会将该链表转换为红黑树,否则,如果数组长度小于 64,则优先进行扩容。

    6. 检查是否需要扩容:在每次添加元素后,都会检查 size 是否超过了 threshold(阈值 = capacity * load factor),如果超过了,则调用 resize() 方法进行扩容,扩容会创建一个新数组,大小是原数组的 2 倍,并重新计算所有元素的在新数组中的位置,这个过程叫 rehash。”


并发编程

这是区分中高级工程师的关键,考察候选人对多线程问题的理解和解决能力。

volatile 关键字

  • 核心考点

    • volatile 的两大特性:保证可见性禁止指令重排序
    • volatilesynchronized 的区别。
    • volatile 的使用场景(状态标记量、单例模式的双重检查锁)。
  • 典型题目

    1. volatile 关键字的作用是什么?能保证原子性吗?
    2. volatilesynchronized 的区别?
    3. 为什么双重检查锁实现的单例模式要用 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() 这行代码并非原子操作,大致可以分解为三步:

      1. memory = allocate(); // 分配对象内存
      2. ctorInstance(memory); // 初始化对象
      3. instance = memory; // 建立 instance 引用指向分配的内存地址

      由于指令重排序,JVM 可能会先执行 1 和 3,再执行 2,另一个线程在第一次检查 if (instance == null) 时,发现 instance 已经不为 null 了(因为它指向了分配的内存,但对象还未初始化),就会返回一个未初始化完成的 instance 对象,导致程序出错。

      volatile 的作用volatile 会禁止 2 和 3 的重排序,保证了 instance 引用在指向内存地址时,对象一定已经被初始化完毕了。

  • 优秀回答范例 (以“volatile 的作用”为例)

    volatile 是 Java 并发编程中一个轻量级的同步机制,它主要有两大核心作用:

    1. 保证变量的可见性

      • 问题背景:在 Java 内存模型中,每个线程都有自己的工作内存,线程对变量的操作都在工作内存中进行,然后同步回主内存,这导致了“可见性”问题:一个线程修改了变量,其他线程可能看不到最新的值。
      • volatile 的解决方案:当一个变量被 volatile 修饰后:
        • 写操作:当线程修改 volatile 变量时,JMM 会强制将该线程工作内存中的值立刻刷新到主内存。
        • 读操作:当线程读取 volatile 变量时,JMM 会强制让线程从主内存中读取最新值,而不是使用工作内存中的缓存副本。
      • volatile 确保了线程间对变量的修改是立即可见的。
    2. 禁止指令重排序

      • 问题背景:为了优化性能,编译器和处理器可能会对指令进行重排序,但在并发场景下,重排序可能会破坏代码的逻辑。
      • volatile 的解决方案volatile 关键字会插入一个“内存屏障”,内存屏障可以禁止其前后的指令进行重排序优化,保证了程序的执行顺序严格按照代码的顺序来。
      • 经典应用:在双重检查锁实现的单例模式中,volatile 防止了 new Singleton() 操作(分配内存、初始化、建立引用)的重排序,避免了其他线程拿到一个未初始化完成的实例。

    volatile 的局限性

    • 不保证原子性volatile 只能保证单个读/写操作的原子性,像 i++ 这样的复合操作,volatile 无法保证其原子性,必须使用 synchronizedjava.util.concurrent.atomic 包下的原子类。

    synchronized 的区别

    • synchronized 是一个锁机制,它不仅能保证可见性,还能保证原子性,同时会阻塞线程,是重量级的。
    • volatile 只是一个关键字,它通过内存屏障来保证可见性和有序性,不会阻塞线程,是轻量级的。
    • synchronized 可以修饰代码块和方法,而 volatile 只能修饰变量。”

JVM (Java 虚拟机)

JVM 是 Java 程序的运行基石,理解 JVM 能写出更高性能、更稳定的代码。

Java 内存模型与运行时数据区

  • 核心考点

    • 运行时数据区:程序计数器、Java 虚拟机栈、本地方法栈、堆、方法区。
    • 各区域的作用、是否线程共享、是否会发生 OutOfMemoryErrorStackOverflowError
    • 垃圾回收:GC Roots、可达性分析算法、常见的垃圾回收器(Serial, Parallel, CMS, G1, ZGC)。
    • 类加载机制:加载、验证、准备、解析、初始化。
  • 典型题目

    1. 画一下 Java 内存模型(运行时数据区)。
    2. 什么情况下会触发 OutOfMemoryError?什么情况下会触发 StackOverflowError
    3. 简述一下垃圾回收的算法和常见的垃圾回收器。
    4. 讲一下类加载的双亲委派模型。
  • 深度剖析

    • 运行时数据区
      • 程序计数器:记录当前线程执行的字节码行号,是唯一一个不会在 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 时间极短(毫秒甚至微秒级)。
    • 双亲委派模型
      • 工作流程:当一个类加载器收到类加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成加载请求时,子加载器才会尝试自己去加载。
      • 核心作用
        1. 安全性:防止核心 API 被篡改,如果有人自己写了一个 java.lang.String 类,双亲委派模型会保证加载的是 rt.jar 中的核心 String 类,而不是用户自定义的,保证了 Java 运行的安全稳定。
        2. 避免重复加载:如果同一个类被多个类加载器加载,会存在于不同的命名空间中,但双亲委派模型保证了类只会在一个加载器中被加载,避免了内存浪费。
  • 优秀回答范例 (以“JVM 内存模型”为例)

    “Java 虚拟机的运行时数据区是 Java 程序运行时的内存划分,主要包括以下几个部分:

    1. 程序计数器

      • 作用:是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器,它记录了下一条要执行的 JVM 指令的地址。
      • 特点:是线程私有的,每个线程都有自己独立的计数器,它的生命周期与线程相同。唯一一个在 JVM 规范中没有规定任何 OutOfMemoryError 情况的区域。
    2. Java 虚拟机栈

      • 作用:描述的是 Java 方法执行的内存模型,每个方法在执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
      • 特点:是线程私有的,生命周期与线程相同。
      • 异常
        • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverflowError
        • 如果虚拟机栈可以动态扩展(大部分 JVM 可扩展),但在扩展时无法申请到足够的内存,会抛出 OutOfMemoryError
    3. 本地方法栈

      • 作用:与虚拟机栈类似,但它为虚拟机使用到的 Native 方法(非 Java 语言实现的方法)服务。
      • 特点:也是线程私有的。
      • 异常:同样会抛出 StackOverflowErrorOutOfMemoryError
    4. Java 堆

      • 作用:是 Java 内存管理中最大的一块,被所有线程共享,它唯一的目的是存放对象实例和数组,几乎所有对象的实例都在这里分配内存。
      • 特点:是垃圾收集器管理的主要区域,因此也被称为“GC 堆”。
      • 异常:如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OutOfMemoryError
    5. 方法区

      • 作用:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据,它逻辑上是堆的一部分,但为了与 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 的区别。
  • 典型题目

    1. 解释一下什么是 IoC 和 DI?
    2. Spring 是如何管理 Bean 的?Bean 的生命周期是怎样的?
    3. @Autowired 是如何实现自动注入的?默认是按类型还是按名称?
    4. 说说你对 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 容器,其本质就是一个实现了依赖注入功能的容器。
    • Bean 的生命周期
      1. 实例化:通过反射创建 Bean 对象。
      2. 填充属性:为 Bean 的属性(依赖)进行赋值(注入)。
      3. 初始化
        • 调用 BeanNameAwaresetBeanName()
        • 调用 BeanFactoryAwaresetBeanFactory()
        • 调用 ApplicationContextAwaresetApplicationContext()
        • 调用 BeanPostProcessorpostProcessBeforeInitialization()
        • 调用 Bean 自身的 @PostConstruct 方法和 init-method 配置的方法。
        • 调用 BeanPostProcessorpostProcessAfterInitialization()
      4. 使用:Bean 准备就绪,可以被应用使用了。
      5. 销毁
        • 调用 @PreDestroy 方法和 destroy-method 配置的方法。
        • 容器关闭时,Bean 被销毁。
    • @Autowired vs @Resource
      • 来源不同@Autowired 是 Spring 框架的注解;@Resource 是 JSR-250 规范的注解(JDK 自带)。
      • 注入方式不同
        • @Autowired:默认按类型进行注入,如果找到多个同类型的 Bean,会再按名称(@Qualifier)或属性名进行匹配,可以配合 @Qualifier 精确指定 Bean 的名称。
        • @Resource:默认按名称进行注入,它会先在容器中查找与属性名相同的 Bean,找不到再按类型查找,可以通过 name 属性显式指定 Bean 的名称。
      • 支持位置不同@Autowired 可以用于构造器、方法、字段、参数;@Resource 主要用于字段和 setter 方法。
  • 优秀回答范例 (以“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)负责上菜(注入依赖)。”

总结与建议

  1. 基础为王equals/hashCodeHashMapvolatile、JVM 内存模型这些基础概念,一定要理解透彻,能够用自己的话清晰地讲出来。
  2. 知其然,更要知其所以然:不要只背答案,不要只背 HashMapput 流程,要理解为什么要有扰动函数、为什么容量要是 2 的幂次方、JDK 1.8 为什么要改成红黑树和尾插法。
  3. 框架原理:对于 Spring、MyBatis 等框架,要理解其核心设计思想和实现原理,而不仅仅是会用 @Autowired
  4. 项目经验:将理论知识与你的实际项目经验结合起来,在项目中遇到了什么并发问题,你是如何用 volatilesynchronized 解决的?项目中的性能瓶颈是什么,你是如何通过分析 JVM 和 GC 来优化的?
  5. 沟通与表达:面试不仅是考察知识,更是考察沟通能力,回答问题时,逻辑要清晰,语言要流畅,最好能使用“总-分-总”的结构,先给出结论,再分点阐述,最后总结。

祝你面试顺利!

分享:
扫描分享到社交APP
上一篇
下一篇