面试真题总结笔记-Java核心基础篇1.0

6 阅读12分钟

1. 集合框架:  HashMap 的底层数据结构是怎样的?它是如何解决哈希冲突的?在 JDK 1.8 之后做了什么优化?

📝 你的回答复述

  1. 数据结构:JDK 1.7 之前是 “数组 + 链表”;JDK 1.8 之后是 “数组 + 链表 + 红黑树”。
  2. 解决冲突:通过设置负载因子,超过就扩容;如果发生冲突,就转成链表。

⭐评分:65 / 100 (及格,但细节扣分严重)

点评

回答方向对数据结构的演变说得清楚,但对 “如何解决哈希冲突” 存在概念混淆,将 “扩容机制” 当成 “解决冲突的手段”,实际扩容是为解决容量不足,直接解决冲突的手段是拉链法(链表)和红黑树。

🔍详细解析与扣分点

  1. 数据结构(+20 分)

    • 回答正确,准确指出 1.7 和 1.8 的结构差异。
    • 加分细节:若补充 “当链表长度大于 8 且数组长度大于 64 时,链表转红黑树”,分数会更高。
  2. 解决哈希冲突的逻辑(-20 分)

    • 扣分点:“通过设置动态因子,当超过该动态因子就进行扩容” 存在错误逻辑绑定。
    • 扩容是当Map里元素太多(size > capacity * loadFactor),为避免哈希冲突概率进一步增加的预防性措施。
    • 解决冲突是指两个不同 Key 算出相同数组下标时的处理办法,正确逻辑是采用链地址法(拉链法),在该数组位置挂链表串联冲突元素。
  3. 缺失的关键点(-15 分)

    • 未提及hash()方法中高 16 位与低 16 位异或的扰动函数操作,该操作可使 Hash 值分布更均匀,减少冲突。
    • 未提及 1.8 将链表插入方式从 “头插法” 改为 “尾插法”,以避免多线程扩容时的死循环问题。

💡参考答案(面试满分版)

面试官您好,HashMap 解决哈希冲突主要采用了 “数组 + 链表 / 红黑树” 的结构,具体流程如下:

  1. 核心机制(拉链法)

    • 放入键值对时,根据 Key 的hashCode计算哈希值,再通过哈希值与数组长度取模运算,确定元素应放的数组 “桶” 位置。

    • 发生冲突(桶位置已有元素)时:

      • JDK 1.8 之前:采用头插法,将新元素插入链表头部。
      • JDK 1.8 之后:采用尾插法,将新元素插入链表尾部。
  2. 性能优化(红黑树)

    • 若桶中链表长度过长(默认超过 8 个),查询效率会从 O (1) 退化为 O (n)。当链表长度大于 8 且数组长度大于 64 时,链表自动转换为红黑树,将查询效率提升回 O (log n)。
    • 反之,若红黑树节点数小于 6,会退化回链表。
  3. 辅助手段(扰动与扩容)

    • 扰动函数:计算 hash 值时,不直接使用key.hashCode(),而是将高 16 位与低 16 位进行异或运算,使哈希分布更均匀,减少冲突概率。
    • 扩容机制:当元素数量超过 “容量 × 负载因子”(默认 0.75)时,HashMap 会扩容(变为原来的 2 倍)。扩容是为降低哈希表的装载因子,减少哈希冲突发生概率,但不是直接解决冲突的手段。

🚀下一步建议

将 “扩容” 和 “解决冲突” 两个概念分开讲,分数可提升至 85+。

2. ConcurrentHashMap 是如何保证线程安全的?它和 Hashtable 有什么区别?

📝你的回答复述

  1. ConcurrentHashMap 机制

    • JDK 1.7:采用了 CAS + synchronized 分段锁。
    • JDK 1.8:采用了更细粒度的锁(锁链表或红黑树节点)。
  2. 与 Hashtable 的区别

    • 性能:ConcurrentHashMap 性能更优。
    • 锁粒度:Hashtable 锁整个集合(全表锁),导致查询和插入都会阻塞;ConcurrentHashMap 锁粒度更细。

⭐ 评分:70 / 100 (概念混淆,但核心点到了)

总体点评

回答抓住 “锁粒度” 核心,但将 JDK 1.7 和 1.8 的实现机制记反,且对 Hashtable 读操作是否阻塞的描述不够准确。

🔍 详细解析与扣分点

  1. 版本机制记反(-20 分)

    • 你的回答:1.7 是 CAS + synchronized;1.8 是细粒度锁。

    • 实际情况:

      • JDK 1.7:使用 Segment 分段锁,继承自ReentrantLock,将整个数组分成 16 个段,锁的粒度是段。
      • JDK 1.8:采用CAS + synchronized,取消了 Segment,直接利用 synchronized 锁住链表的头节点或红黑树的根节点,粒度细化到桶。
  2. Hashtable 的读操作(-5 分)

    • 你的回答:查询会被全部阻塞。
    • 实际情况:Hashtable 的get方法也是synchronized修饰的,读操作确实需要获取锁,高并发读时性能差。更严谨的说法是,因锁住整个对象,读写、写写操作都会互斥。
  3. 缺失的关键点(-5 分)

    • 未提及 1.8 中,桶位为空时使用 CAS 进行无锁插入,发生冲突时才升级为 synchronized 锁的无锁化操作。
    • 未提及 1.8 中利用volatile修饰 Node 的 val 和 next 指针保证可见性,实现读操作完全无锁。

💡参考答案(面试满分版)

面试官您好,ConcurrentHashMap 在不同版本的实现机制不一样,它和 Hashtable 的主要区别在于锁的粒度和实现方式:

  1. ConcurrentHashMap 的实现机制

    • JDK 1.7 版本:

      • 采用 Segment 分段锁机制。
      • 把整个哈希表分成多个 Segment(默认 16 个),每个 Segment 继承自ReentrantLock
      • 原理:每个 Segment 独立加锁。不同线程访问不同 Segment 时可并发进行,访问同一个 Segment 时才会互斥。提高了并发度,但结构较复杂。
    • JDK 1.8 版本:

      • 放弃了 Segment,采用CAS + synchronized结合的方式。
      • 原理:插入元素时,若目标桶为空,使用 CAS 无锁插入;若桶有元素(发生冲突),则使用 synchronized 锁住该桶的头节点(链表或红黑树的根节点)。
      • 优势:锁的粒度从 “段” 细化到 “节点”,并发性能更高。且利用 volatile 变量保证读操作的可见性,使读操作完全不需要加锁。
  2. 与 Hashtable 的区别

    • 锁粒度不同:

      • Hashtable:使用synchronized修饰方法,锁的是整个对象(整张表)。同一时刻,只能有一个线程进行读或写操作,其他线程全部阻塞,性能低。
      • ConcurrentHashMap:锁粒度非常细(1.7 是段,1.8 是桶)。允许多个线程同时读取,写入时也只锁定特定的桶,大大减少线程阻塞。
    • 迭代器一致性:

      • Hashtable 的迭代器是强一致性的,遍历时修改会抛出ConcurrentModificationException
      • ConcurrentHashMap 的迭代器是弱一致性的,遍历时修改不会抛异常,基于创建迭代器时的状态进行遍历。

🚀总结与修正

对 “ConcurrentHashMap 锁粒度更细,Hashtable 锁整个表” 的核心理解正确。需修正记忆:

  • 1.7:分段锁,锁的是 Segment(粗)。
  • 1.8:CAS + synchronized,锁的是 Node(细)。

3. 请解释一下线程池的几个核心参数(corePoolSizemaxPoolSizeworkQueue)。如果队列满了,会发生什么?

📝你的回答复述

  1. 参数定义:解释了corePoolSize(核心线程数)、maxPoolSize(最大线程数)、workQueue(工作队列)。

  2. 执行流程

    • 懒加载:线程池空闲时,核心线程不会被创建(默认情况下)。
    • 核心线程:有任务时先创建核心线程执行。
    • 队列缓冲:核心线程满后,任务进入工作队列。
    • 扩容线程:队列满后,创建非核心线程(直到最大线程数)。
    • 拒绝策略:超过最大线程数后,执行拒绝策略。

⭐评分:90 / 100 (非常标准的回答)

总体点评

回答准确描述了线程池的 “工作窃取” 逻辑和参数关系,虽有关于核心线程创建时机的细微表述可优化,但整体逻辑清晰,达到 12k 薪资要求标准。

🔍详细解析与优化点

  1. 核心线程的创建时机(+10 分,但有微小瑕疵)

    • 你的回答:“当线程池未被调用的时候,核心线程不会被创建…… 这个时候线程池才会创建核心线程。”
    • 解析:默认情况下正确(预热机制)。
    • 优化点:默认情况下,线程池创建后为空,提交第一个任务时才创建核心线程。也可通过调用prestartCoreThread()方法让线程池启动时就创建好核心线程(预热),面试中不提此点通常也没问题,因描述的是最常见默认行为。
  2. 队列满了之后的判断(+20 分)

    • 你的回答:“当工作队列满了的时候,就会去创建非核心线程。”
    • 解析:逻辑精准,准确指出只有队列满了才会创建非核心线程,体现对ThreadPoolExecutor源码流程(execute方法)的理解。
  3. 拒绝策略的触发条件(+20 分)

    • 你的回答:“当超过最大线程数,则会使用拒绝策略。”
    • 解析:完美,触发拒绝策略的条件是线程数达到最大值且队列已满。

💡 参考答案(面试满分版)

面试官您好,线程池的执行流程遵循 “核心线程 -> 任务队列 -> 非核心线程 -> 拒绝策略” 的原则:

  1. 核心参数含义

    • corePoolSize:核心线程数。线程池长期维持的线程数量,即使线程空闲也不会被销毁(除非设置了allowCoreThreadTimeOut)。
    • maximumPoolSize:最大线程数。线程池中允许存在的最大线程数量(核心 + 非核心)。
    • workQueue:工作队列。用于保存等待执行任务的阻塞队列(如ArrayBlockingQueueLinkedBlockingQueue)。
  2. 任务提交流程

    • 步骤一(核心线程):提交新任务时,若当前运行线程数少于corePoolSize,线程池立即创建新的核心线程执行任务,不会将任务放入队列。
    • 步骤二(任务队列):若当前线程数 >=corePoolSize,则将任务加入workQueue。队列未满,任务等待;队列已满,进入步骤三。
    • 步骤三(非核心线程):若队列已满,且当前线程数 <maximumPoolSize,线程池创建非核心线程立即执行新任务(执行刚提交的任务,而非队列里的旧任务)。
    • 步骤四(拒绝策略):若队列已满,且线程数也达到maximumPoolSize,线程池饱和,触发拒绝策略(RejectedExecutionHandler)。

🚀 总结

回答逻辑清晰,无概念性错误,是多线程面试高频题的良好回答方式。

4. 什么是反射?它在我们常用的框架(如 Spring 或 MyBatis)中有哪些典型应用?

📝 你的回答复述

  1. 定义:反射是 Java 程序在运行时动态获取类型信息、行为的一种方式。
  2. 应用:Spring Boot 框架中的 AOP 面向切面使用到了反射机制。

⭐ 评分:60 / 100 (定义尚可,应用举例严重偏题 / 概念混淆)

总体点评

回答对反射定义抓到 “运行时” 和 “动态” 关键点,但应用场景回答存在严重概念偏差:

  1. AOP 的核心实现技术是动态代理(JDK Proxy 或 CGLIB),虽动态代理底层可能用到反射,但说 “AOP 使用反射” 易被认为概念不清,AOP 更准确的底层是代理模式。
  2. 避开了 IOC 和 ORM 映射等核心应用,选择易混淆的 AOP,导致分数大打折扣。

🔍 详细解析与扣分点

  1. 反射的定义(+15 分)

    • 你的回答:“运行时动态获取类型信息”。
    • 解析:正确,反射允许在不知道具体类的情况下,运行时加载类、调用方法、访问字段。
  2. 应用场景:AOP(-20 分)

    • 扣分点:因果倒置。
    • 反射主要用于 “调用方法” 或 “访问字段”。
    • AOP 主要用于 “功能增强” 和 “解耦”。
    • 关系:AOP 通过动态代理生成代理对象,在代理对象的拦截方法(如invoke)中确实会使用反射(method.invoke)调用原对象方法,但 AOP 的灵魂是代理,而非反射。
    • 面试后果:回答 “AOP 用反射” 可能被追问动态代理和反射的区别,答不上来会被判定为背题且不懂原理。
  3. 缺失的核心考点(-15 分)

    • Spring IOC (依赖注入):Spring 容器通过反射(Class.forName)加载类,通过反射创建 Bean 实例(newInstance或构造器),并通过反射(Field.set)将 Bean 注入到需要它的类中(即@Autowired的原理)。
    • MyBatis (ORM 映射):MyBatis 通过反射将数据库查询出的ResultSet结果集,自动映射(set)到实体类(POJO)的对象属性中。

💡 参考答案(面试满分版)

面试官您好,反射是 Java 提供的一种能力,允许程序在运行时动态地加载类、获取类的结构(如属性、方法),并能操作这些属性和方法(如创建实例、调用方法)。

它在框架中的应用非常广泛,主要结合 Spring 和 MyBatis 谈谈:

  1. Spring 框架的 IOC(控制反转)容器

    • 实例化 Bean:Spring 启动时,读取配置文件或注解(如@Component)。因不知道类的具体名字,需使用Class.forName()通过类的全限定名动态加载类,再利用反射调用构造器创建 Bean 实例。
    • 依赖注入:容器需将一个 Bean 赋值给另一个 Bean 的字段时(例如@Autowired),利用反射获取该字段的引用,通过Field.set()方法将实例化的对象注入进去,甚至可无视访问修饰符(通过setAccessible(true))。
  2. MyBatis 框架的 ORM 映射

    • 结果集映射:执行 SQL 查询数据库返回结果集后,MyBatis 需将数据自动填充到定义的 Java 实体类对象中。通过反射获取实体类的所有属性,根据数据库字段名找到对应的属性,调用属性的 setter 方法或直接设置属性值,完成数据库记录到 Java 对象的自动转换。
  3. 补充:通用的动态调用

    • 框架通常需根据配置文件或注解决定调用哪个类的哪个方法(例如 SpringMVC 的请求映射),这种 “动态决定” 的能力完全依赖于反射。

🚀总结与修正

  • 不要说 AOP 是反射,AOP 的底层是动态代理。
  • 要说 IOC 和 ORM,反射最核心的作用是 “把数据塞进对象”(MyBatis)和 “把对象塞进容器”(Spring)。

编辑分享

除了HashMap,还有哪些常用的集合框架?

如何选择合适的集合框架?

如何优化HashMap的性能?