【无标题】

01.请聊一下java的集合类,以及在实际项目中你是如何用的

注意说出集合体系,常用类接口实现类
加上你所知道的高并发集合类,JUC参照集合增强内容在实际项目中引用, 照实说就好了
问集合的引子... ...

✅ 一、Java 集合体系结构(先说框架)

Java 集合框架主要分为两大体系:

1️⃣ Collection(单列集合)

常用子接口:

  • List :有序、可重复
    👉 实现类:ArrayListLinkedList
  • Set :无序、不重复
    👉 实现类:HashSetLinkedHashSetTreeSet
  • Queue / Deque :队列
    👉 实现类:ArrayDequePriorityQueue

2️⃣ Map(键值对集合)

  • 核心接口:Map

  • 常用实现类:

    • HashMap
    • LinkedHashMap
    • TreeMap
    • Hashtable(已过时)

✅ 二、常用集合类特点(必会)

List 体系

特点 底层
ArrayList 查询快,增删慢 动态数组
LinkedList 增删快,查询慢 双向链表

Set 体系

特点
HashSet 无序、不重复
LinkedHashSet 按插入顺序
TreeSet 排序存储

Map 体系

特点
HashMap 无序,允许null
LinkedHashMap 按插入或访问顺序
TreeMap key自动排序

✅ 三、高并发集合(JUC包)

JUC(java.util.concurrent)中提供了线程安全集合:

Map

  • ConcurrentHashMap
    👉 高并发场景下替代 HashMap

List

  • CopyOnWriteArrayList

Set

  • CopyOnWriteArraySet

Queue

  • BlockingQueue 系列:

    • ArrayBlockingQueue
    • LinkedBlockingQueue
    • PriorityBlockingQueue

✅ 四、实际项目中如何使用(重点!)

你可以这样答(很真实,也很安全)👇:

在项目中主要使用的是 List 和 Map 比较多。

比如:

  • List 存储查询出来的多条数据结果,比如用户列表、订单列表。
  • Map 作为缓存或快速查找结构,比如用 userId 作为 key,对应用户对象作为 value。

在多线程场景下,比如使用线程池处理任务时,如果多个线程需要共享数据,我会使用 ConcurrentHashMapCopyOnWriteArrayList,避免使用普通的 HashMapArrayList 产生线程安全问题。

如果对顺序有要求,比如需要保持插入顺序,我会使用 LinkedHashMapLinkedHashSet

你也可以结合你学过的项目说,例如(模板):

👉 通用项目版回答:

在我做的后端项目中,比如用户管理模块,查询数据库返回多条用户数据时会用 List<User> 接收。

对于需要根据 id 快速定位对象的场景,我会使用 Map<Long, User>

如果是并发访问的数据,比如缓存统计信息,我会使用 ConcurrentHashMap 来保证线程安全。


✅ 五、面试官为什么爱问这题?

他想引出:

  • 👉 HashMap 原理
  • 👉 ConcurrentHashMap 原理
  • 👉 ArrayList 扩容
  • 👉 HashSet 原理

你答完这题,他很可能追问:

❓ HashMap 和 ConcurrentHashMap 区别?

❓ ArrayList 和 LinkedList 区别?

❓ HashMap 底层结构?


🎯 一句话精简版(极简背诵)

Java集合分为 Collection 和 Map 两大体系。

Collection 包括 List、Set、Queue;Map 是键值对集合。

常用实现类有 ArrayList、LinkedList、HashSet、HashMap、TreeMap、LinkedHashMap。

在高并发场景下会使用 JUC 提供的 ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue。

实际项目中常用 List 存储查询结果,用 Map 做快速映射或缓存结构。


如果你愿意,我可以帮你整理成:

✅【面试背诵版】

✅【追问:HashMap底层原理】

✅【追问:ConcurrentHashMap】

✅【追问:ArrayList扩容】

你现在可以继续:

👉 下一题你问我

👉 你要我给你模拟:"面试官追问HashMap" 吗?

Hashmap为什么要使用红黑树? 在jdk1.8版本后,java对HashMap做了改进,在链表长度大于8的时候,将后面的数据存在红黑树中, 以加快检索速度 红黑树虽然本质上是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树 相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。加快检索速率。 ```


标准回答版:

HashMap 在 JDK1.8 之后引入红黑树,主要是为了解决哈希冲突严重时,链表过长导致查询效率下降的问题。

在 JDK1.8 之前,HashMap 底层是数组加链表结构,当某个桶中的链表很长时,查找元素需要遍历链表,时间复杂度最坏是 O(n)。

从 JDK1.8 开始,当链表长度超过 8 时,会将链表转换为红黑树。红黑树是一种自平衡的二叉查找树,通过节点着色和旋转来保持相对平衡,从而保证查找、插入、删除操作的时间复杂度最坏为 O(log n)。

这样在哈希冲突比较严重的情况下,可以显著提高 HashMap 的检索性能。


一句话精简版(短答用):

为了避免链表过长导致查询效率退化为 O(n),JDK1.8 在链表长度超过 8 时引入红黑树,使查找复杂度稳定在 O(log n),提升了极端情况下的性能。


如果面试官再追一句:

❓**"为什么是 8?"**

你可以补一句(加分):

8 是经验值,在链表和红黑树之间做了性能和空间的折中,小于 8 用链表更节省空间,大于 8 转树更划算。


✅ 一、链表 → 红黑树(树化)的条件

在 JDK 1.8 中,满足 两个条件 才会树化:

条件 1:

👉 同一个桶中的链表长度 ≥ 8

源码里常量:

java 复制代码
static final int TREEIFY_THRESHOLD = 8;

条件 2:

👉 HashMap 的数组长度 ≥ 64

源码里常量:

java 复制代码
static final int MIN_TREEIFY_CAPACITY = 64;

⭐ 也就是说:

必须同时满足:

  • 链表长度 ≥ 8
  • 数组容量 ≥ 64

才会把链表转换为红黑树。


❓ 如果链表 ≥ 8,但数组长度 < 64 呢?

不会树化,而是:

👉 优先扩容数组(resize)

原因:

很可能是数组太小导致冲突多,先扩容比建红黑树更划算。

你可以这样说:

当链表长度超过 8 但数组容量还比较小(小于 64)时,HashMap 会优先进行扩容,而不是直接树化。


✅ 二、红黑树 → 链表(退化条件)

当元素减少或扩容时,如果红黑树中节点数量变少:

👉 当节点数 ≤ 6 时,会退化回链表

源码常量:

java 复制代码
static final int UNTREEIFY_THRESHOLD = 6;

这是为了:

  • 小数据量时,链表更省内存
  • 树结构维护成本更高

🎯 面试标准回答(可背)

在 JDK1.8 中,HashMap 从链表转成红黑树需要同时满足两个条件:

第一是桶中的链表长度达到 8;

第二是当前数组容量达到 64。

如果链表长度超过 8 但数组容量还小于 64,则不会树化,而是优先进行扩容。

当红黑树中的节点数量减少到 6 以下时,又会退化为链表。


HashMap 的同一个数组中,不同下标对应的桶里,

存储结构可以不一样:

有的桶是链表结构,

有的桶可能已经转换成红黑树结构。

也就是说,一个 HashMap 里可能同时存在:

text 复制代码
table[0] -> 链表
table[1] -> 红黑树
table[2] -> 单个节点
table[3] -> 空

每个桶是独立判断是否需要树化的:

  • 链表长度 < 8 → 链表
  • 链表长度 ≥ 8 且数组长度 ≥ 64 → 红黑树
  • 红黑树节点 ≤ 6 → 退化回链表

集合类是怎么解决高并发中的问题? 思路 先说一下那些是非安全 普通的安全的集合类 JUC中高并发的集合类 线程非安全的集合类 ArrayList LinkedList HashSet TreeSet HashMap TreeMap 实际开发中我们自己 用这样的集合最多,因为一般我们自己写的业务代码中,不太涉及到多线程共享同一个集合的问题 线程安全的集合类 Vector HashTable 虽然效率没有JUC中的高性能集合高,但是也能够适应大部分环境 高性能线程安全的集合类

一、先区分:哪些集合是线程不安全的

Java 中大部分常用集合类本身是线程不安全的,比如:
ArrayListLinkedListHashSetTreeSetHashMapTreeMap

这些集合在多线程同时读写时,可能会出现:

  • 数据丢失
  • 数据不一致
  • 抛出 ConcurrentModificationException

不过在实际开发中:

我们日常业务代码里,大多数场景是单线程访问集合,所以通常还是优先使用这些普通集合类,因为性能更好。


二、早期线程安全集合(通过 synchronized 实现)

Java 早期提供了一些线程安全集合,比如:
VectorHashtable

它们的特点是:

  • 内部方法使用 synchronized 修饰
  • 每次操作都会加整把锁

优点:

✅ 使用简单

✅ 能保证线程安全

缺点:

❌ 并发性能差

❌ 锁粒度太大

在高并发场景下性能不理想,现在已经不太推荐使用。


三、JUC 中的高并发集合类(重点)

java.util.concurrent 包中,提供了高性能的线程安全集合:

1️⃣ Map

  • ConcurrentHashMap

特点:

  • 使用更细粒度的锁或 CAS
  • 读操作几乎无锁
  • 写操作冲突小
    👉 适合高并发场景下替代 HashMap

2️⃣ List / Set

  • CopyOnWriteArrayList
  • CopyOnWriteArraySet

特点:

  • 读不加锁
  • 写时复制新数组
    👉 适合:读多写少的场景

3️⃣ Queue

  • BlockingQueue 系列:

    • ArrayBlockingQueue
    • LinkedBlockingQueue

特点:

  • 支持阻塞
  • 常用于生产者消费者模型、线程池任务队列

四、集合类解决高并发问题的总体思路

你可以这样总结:

Java 集合类解决高并发问题主要有三种方式:

第一种是通过 synchronized 保证线程安全,比如 Vector、Hashtable;

第二种是通过更细粒度锁或 CAS 实现,比如 ConcurrentHashMap;

第三种是通过读写分离的方式,比如 CopyOnWriteArrayList,适合读多写少场景。


🎯 面试标准总结版(可背)

Java 中普通集合类大多是线程不安全的,比如 ArrayList、HashMap 等,在多线程场景下需要额外处理。

早期线程安全集合如 Vector、Hashtable 通过 synchronized 保证线程安全,但并发性能较差。

JUC 包中提供了高性能线程安全集合,如 ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue,它们通过更细粒度锁、CAS 或读写分离来提高并发性能。

在实际开发中,如果涉及多线程共享集合,我会优先使用 JUC 提供的并发集合类。


简述一下自定义异常的应用场景? 借助异常机制,我们可以省略很多业务逻辑上的判断处理,直接借助java的异常机制可以简化业务逻辑判断 代码的编写 1当你不想把你的错误直接暴露给前端或者你想让前端从业务角度判断后台的异常,这个时候自定义异常 类是你的不二选择 2 虽然JAVA给我们提供了丰富的异常类型,但是在实际的业务上,还有很多情况JAVA提供的异常类型不能准 确的表述出我们业务上的含义 3 控制项目的后期服务 ... ...

自定义异常的应用场景:

  1. 隐藏底层技术异常,统一对外返回业务错误

    将如 SQLExceptionNullPointerException 等技术异常,转换为自定义业务异常(如 UserNotFoundExceptionOrderStatusException),避免将底层实现细节直接暴露给前端,同时便于前端按业务含义处理。

  2. 表达更明确的业务语义

    Java 内置异常主要描述技术错误,无法准确体现业务规则(如"库存不足""账号被封禁"),通过自定义异常可以精确描述业务失败原因,提高代码可读性和可维护性。

  3. 简化业务逻辑判断

    对关键校验失败场景(如参数不合法、状态不符合操作条件),直接抛出自定义异常,减少大量 if-else 判断,使主流程代码更加清晰。

  4. 配合全局异常处理,实现统一错误响应

    结合全局异常处理器(如 @ControllerAdvice),对不同自定义异常返回统一格式的错误码和错误信息,便于前后端交互和问题定位。

  5. 便于后期扩展与异常分类管理

    可以按模块或业务类型定义异常体系(如订单异常、权限异常),方便日志分析、问题追踪和系统扩展。

描述一下Object类中常用的方法? 参照面向对象章节toString hashCode equals clone finalized wait notify notifyAll ... ... 解释每个方法的作用 toString 定义一个对象的字符串表现形式 Object类中定义的规则是 类的全路径名+@+对象的哈希码 重 写之后 我们可以自行决定返回的字符串中包含对象的那些属性信息 ... clone >>>返回一个对象的副本 深克隆 浅克隆 原型模式 重写时实现Cloneable finalized GC 会调动该方法 自救


1. toString()

作用: 返回对象的字符串表示形式。
默认实现:
类的全限定名 + @ + 对象的哈希值(16进制)

例如:com.xxx.User@1a2b3c

意义:

重写后可以自定义输出对象的属性信息,方便日志打印和调试。


2. equals(Object obj)

作用: 判断两个对象是否"逻辑相等"。
默认实现: 比较的是对象地址(==)。

意义:

通常需要重写,用来比较对象的内容是否相同(如两个 User 的 id 是否一致)。


3. hashCode()

作用: 返回对象的哈希码值。
用途: 用于基于哈希的数据结构,如 HashMapHashSet

规则:

  • 如果两个对象 equals 相等,则 hashCode 必须相等
  • hashCode 相等,equals 不一定相等

意义:

一般和 equals() 一起重写,保证集合类正常工作。


4. clone()

作用: 复制当前对象,生成一个新对象。

特点:

  • 默认是浅拷贝
  • 使用时必须实现 Cloneable 接口并重写 clone()

应用场景:

  • 对象快速复制
  • 原型模式
  • 区分浅拷贝与深拷贝(引用对象是否共享)

5. finalize()

作用: 在对象被 GC 回收前调用一次(不保证一定执行)。

特点:

  • 可用于对象"自救"(重新建立引用)
  • 只会被调用一次

问题:

  • 不可靠、性能差
  • Java 9 以后已被废弃,不推荐使用

6. wait()

作用: 让当前线程进入等待状态,并释放对象锁。
前提: 必须在 synchronized 中使用。

用途:

用于线程间通信(生产者-消费者模型)。


7. notify()

作用: 唤醒一个 正在该对象上等待的线程。
前提: 必须在 synchronized 中使用。


8. notifyAll()

作用: 唤醒所有在该对象上等待的线程。


9. getClass()

作用: 获取运行时对象的 Class 对象。
用途: 反射基础。


1.8的新特性有了解过吗? (注意了解其他版本新特征) +JDK 更新认识 · Lambda表达式 · 函数式接口 函数式编程 · 方法引用和构造器调用 · Stream API · 接口中的默认方法和静态方法 · 新时间日期API

可以这样回答 "你了解 JDK 1.8 的新特性吗?顺带说下其他版本的更新认识",既专业又不啰嗦 👇


一、JDK 1.8 的核心新特性

1️⃣ Lambda 表达式

作用: 简化匿名内部类写法,使代码更简洁。

本质是把"方法"当作参数传递。

常用于:集合遍历、线程、回调逻辑。


2️⃣ 函数式接口(Functional Interface)

定义: 只有一个抽象方法的接口。

如:RunnableComparatorCallableConsumerFunction
意义:

为 Lambda 提供类型基础,实现函数式编程思想。


3️⃣ 方法引用 & 构造器引用

形式:

  • 类名::方法名
  • 对象::方法名
  • 类名::new

作用:

当 Lambda 只是调用已有方法时,可以用方法引用进一步简化代码,提高可读性。


4️⃣ Stream API

作用:

对集合数据进行函数式风格的操作(过滤、映射、排序、聚合等)。

特点:

  • 不修改原集合
  • 支持链式调用
  • 支持并行流(parallelStream)

常见操作:
filtermapsortedcollectreduce


5️⃣ 接口中的默认方法和静态方法

  • default 方法:接口可以有方法实现
  • static 方法:接口可以定义工具方法

意义:

  • 解决接口升级兼容问题
  • 支持多实现下的方法复用

6️⃣ 新日期时间 API(java.time)

替代 DateCalendar

常用类:

  • LocalDate
  • LocalTime
  • LocalDateTime
  • Instant
  • DurationPeriod

优势:

  • 线程安全
  • 设计更合理
  • API 更易用

二、对其他版本新特性的基本认识(简述)

▶ JDK 9

  • 模块化系统(JPMS)
  • 集合工厂方法:List.of()Set.of()

▶ JDK 11(LTS)

  • var 用于 Lambda 参数
  • 新增 HttpClient
  • String 新方法:isBlankstrip

▶ JDK 17(LTS)

  • sealed 类(受限继承)
  • switch 表达式增强
  • 更强的 GC 和性能优化

三、总结一句话(面试好用)

JDK 1.8 引入了 Lambda 和函数式接口,使 Java 支持函数式编程;通过 Stream API 提供对集合的声明式处理方式;接口支持默认方法与静态方法;并提供了全新的线程安全日期时间 API。之后版本主要在模块化、语法简化和性能优化方面不断演进。


简述一下Java面向对象的基本特征,继承、封装与多态, 以及你自己的应用? 知识参照面向对象章节 注意单独解释 继承 封装 多态的概念 继承 基本概念解释 后面多态的条件 封装 基本概念解释 隐藏实现细节,公开使用方式 多态 基本概念解释 就是处理参数 提接口 打破单继承 设计模式 设计原则

一、Java 面向对象的三大特征

👉 继承、封装、多态


1️⃣ 继承(Inheritance)

📌 概念

继承是指:

子类可以继承父类的属性和方法,实现代码复用,并在此基础上进行扩展。

使用关键字:

java 复制代码
class Child extends Parent {}

📌 特点

  • Java 是单继承,多实现(一个类只能继承一个父类)

  • 子类可以:

    • 直接用父类方法
    • 重写父类方法
  • 提高代码复用性

📌 多态成立的前提之一

  • 必须存在继承关系

📌 项目中的应用

比如在项目中:

java 复制代码
BaseController
UserController extends BaseController
OrderController extends BaseController
  • 把公共逻辑(返回结果封装、异常处理)放到父类
  • 子类只关心各自业务逻辑
    👉 减少重复代码

Animal a1 = new Cat();

Animal a2 = new Dog();

a1.speak(); // 小猫喵喵叫

a2.speak(); // 小狗汪汪叫

2️⃣ 封装(Encapsulation)

📌 概念

封装是指:

把对象的属性和实现细节隐藏起来,只对外暴露必要的操作方式。

📌 特点

  • 使用 private 隐藏属性
  • 提供 public 方法访问(getter/setter)
  • 避免外部随意修改内部状态

📌 核心思想

👉 隐藏实现细节,暴露使用方式

📌 项目中的应用

例如实体类:

java 复制代码
class User {
    private String password;

    public void setPassword(String password){
        // 可以加校验逻辑
        this.password = password;
    }
}

在业务中:

  • 外部不能直接改 password

  • 只能通过方法修改,便于:

    • 做校验
    • 统一控制规则

👉 提高安全性和可维护性


3️⃣ 多态(Polymorphism)

📌 概念

多态是指:

同一个方法调用,在不同对象上有不同的实现效果。

📌 实现条件

  1. 有继承或实现关系
  2. 父类引用指向子类对象
  3. 方法被重写
java 复制代码
Animal a = new Dog();
a.eat();  // 调用的是 Dog 的实现

📌 作用

  • 面向接口编程
  • 解耦
  • 便于扩展

📌 项目中的应用

比如支付接口:

java 复制代码
interface PayService {
    void pay();
}

class AliPayService implements PayService {}
class WxPayService implements PayService {}

业务代码:

java 复制代码
PayService payService = payFactory.get(type);
payService.pay();

👉 不关心具体实现,只关心接口

👉 新增支付方式只需新增实现类


五、面试标准回答模板(可背)

Java 面向对象的三大特征是继承、封装和多态。

继承是子类复用父类的属性和方法,是实现多态的前提条件之一;

封装是隐藏对象的内部实现细节,只对外提供必要的访问方式,提高安全性和可维护性;

多态是指父类引用指向子类对象时,调用同一方法表现出不同的行为,使程序具备良好的扩展性。

在项目中,我通过继承抽取公共父类,通过封装控制实体属性访问,通过多态实现面向接口编程,比如不同业务策略通过接口+实现类进行解耦。


Java中重写和重载的区别? 联系: 名字相似 都是多个同名方法 重载 在同一个类之中发生的 重写 继承中,子类重写父类方法 1 目的差别 2 语法差别


一、联系(共同点)

重载和重写的联系是:

都是多个同名方法,体现了多态思想。


二、定义

1️⃣ 重载(Overload)

发生位置: 同一个类中
含义:

方法名相同,但参数列表不同的方法。

java 复制代码
class Calculator {
    int add(int a, int b) { return a + b; }
    int add(int a, int b, int c) { return a + b + c; }
    double add(double a, double b) { return a + b; }
}

2️⃣ 重写(Override)

发生位置: 子类中
含义:

子类对父类中已有的方法进行重新实现。

java 复制代码
class Animal {
    public void speak() {
        System.out.println("动物在叫");
    }
}

class Dog extends Animal {
    @Override
    public void speak() {
        System.out.println("小狗汪汪叫");
    }
}

三、区别

① 目的不同

维度 重载 重写
目的 同一功能的不同参数表现 改变父类方法的具体实现
本质 编译期多态 运行期多态

② 语法不同(重点)

维度 重载 重写
方法名 必须相同 必须相同
参数列表 必须不同(数量 / 类型 / 顺序) 必须相同
返回值 可以不同 必须相同或是子类类型
访问修饰符 无限制 不能更严格
抛出异常 无要求 不能抛出更大异常
发生位置 同一个类 子类

四、面试一句话总结(可背)

重载发生在同一个类中,方法名相同、参数列表不同,用于提供同一功能的多种使用方式;

重写发生在继承关系中,子类对父类方法重新实现,用于实现运行时多态。


五、面试常见坑(你要知道)

❌ 参数名不同不算重载

java 复制代码
int add(int a, int b);
int add(int x, int y); // ❌ 不是重载

❌ 仅返回值不同不是重载

java 复制代码
int add(int a, int b);
double add(int a, int b); // ❌ 编译报错

final关键字的用法有哪些

final 在 Java 里非常常考,可以从 修饰变量、方法、类 三个方面来答。


一、final 修饰变量(常量)

1️⃣ 修饰基本类型变量

表示:值不能再改变

java 复制代码
final int a = 10;
a = 20; // ❌ 编译错误

2️⃣ 修饰引用类型变量

表示:引用不能变,但对象内容可以变

java 复制代码
final List<String> list = new ArrayList<>();
list = new ArrayList<>(); // ❌ 不允许
list.add("hello");        // ✅ 可以

👉 final 修饰引用:
地址不能变,内容可以变

3️⃣ 修饰成员变量(常量)

通常:

java 复制代码
public static final int MAX_SIZE = 100;
  • 常量命名全大写
  • 一般在定义时或构造方法中初始化

二、final 修饰方法

表示:

方法不能被子类重写(Override)

java 复制代码
class Parent {
    public final void show() {}
}

class Child extends Parent {
    public void show() {} // ❌ 编译错误
}

使用场景:

  • 核心算法
  • 安全逻辑
  • 不希望子类改变的方法

三、final 修饰类

表示:

类不能被继承

java 复制代码
final class MyClass {}
class Sub extends MyClass {} // ❌

典型例子:

👉 StringIntegerLong


四、面试常见扩展(加分)

🔹 final 参数

java 复制代码
public void test(final int x) {
    // x 不能被重新赋值
}

🔹 final 和多线程

  • final 变量具有可见性保证
  • 常用于不可变对象设计

五、总结一句话(面试版)

final 可以修饰变量、方法和类,修饰变量表示值不可改变,修饰方法表示不能被重写,修饰类表示不能被继承,常用于定义常量、保证安全性和设计不可变对象。


六、常见坑

final 修饰的对象不可变

✔ 错,引用不可变,对象内容可变

final 类中的方法都必须是 final

✔ 错,只是默认不能被重写


**Java中的自增是线程安全的吗,如何实现线程安全的自

增?
i++ ++i
增加synchronized进行线程同步
使用lock、unlock处理Reetrantent 锁进行锁定
AtomicInteger >>> Unsafe >>> cas >>> aba
首先说明,此处 AtomicInteger,一个提供原子操作的 Integer 的类,常见的还有AtomicBoolean、
AtomicInteger、AtomicLong、AtomicReference 等,他们的实现原理相同,区别在与运算对象类型的
不同。令人兴奋地,还可以通过 AtomicReference将一个对象的所有操作转化成原子操作。
我们知道,在多线程程序中,诸如++i 或 i++等运算不具有原子性,是不安全的线程操作之一。通常我们
会使用 synchronized 将该操作变成一个原子操作,但 JVM 为此类操作特意提供了一些同步类,使得使
用更方便,且使程序运行效率变得更高。通过相关资料显示,通常AtomicInteger 的性能是
ReentantLock 的好几倍。**

一、为什么 i++ / ++i 不是线程安全的?

看起来是一行,其实底层分三步:

text 复制代码
1. 读取 i 的值
2. i = i + 1
3. 写回 i

在多线程下可能出现:

线程A读到 i=10

线程B也读到 i=10

都加 1 写回 → 结果还是 11

本该变成 12,却只加了一次

👉 这就是:丢失更新问题

所以:

i++++i 在多线程环境中不是原子操作,不是线程安全的。


二、如何实现线程安全的自增?

✅ 方式1:synchronized

java 复制代码
synchronized(this) {
    i++;
}

特点:

  • 保证同一时刻只有一个线程执行
  • 简单可靠
  • 性能相对较低(阻塞式)

✅ 方式2:ReentrantLock

java 复制代码
lock.lock();
try {
    i++;
} finally {
    lock.unlock();
}

特点:

  • 可中断
  • 可公平锁
  • 性能优于 synchronized(在高并发下)

✅ 方式3:AtomicInteger(推荐)

java 复制代码
AtomicInteger atomic = new AtomicInteger(0);
atomic.incrementAndGet();

底层原理:

复制代码
Unsafe + CAS(Compare And Swap)

流程:

  1. 比较内存中的值是否等于旧值
  2. 相等 → 修改
  3. 不相等 → 自旋重试

👉 无锁、非阻塞

👉 性能高于 synchronized 和 ReentrantLock


三、CAS 相关补充(面试加分)

  • CAS 可能有:

    • ABA 问题
    • 自旋消耗 CPU
  • 可用:

    • AtomicStampedReference
    • AtomicMarkableReference
      解决 ABA 问题

四、常见原子类

  • AtomicInteger
  • AtomicLong
  • AtomicBoolean
  • AtomicReference

👉 可以把对象的更新也变成原子操作


五、面试标准回答版(可背)

i++++i 本身不是原子操作,在多线程环境下不是线程安全的,因为它们包含读、改、写三个步骤。

可以通过 synchronizedReentrantLock 进行加锁来保证线程安全,也可以使用 AtomicInteger,它底层通过 CAS 实现无锁的原子自增,性能通常优于锁机制。


Jdk1.8中的stream有用过吗,详述一下stream的并行操

作原理?stream并行的线程池是从哪里来的?

一、什么是 Stream 并行操作?

Stream 提供两种模式:

  • 串行流stream()
  • 并行流parallelStream()stream().parallel()

并行流会把数据拆分成多个子任务,交给多个线程同时处理,最后再合并结果,从而提高多核 CPU 利用率。


二、并行 Stream 的底层原理

并行流底层使用的是:

👉 Fork/Join 框架(分治思想)

核心思想:

  1. 把一个大任务拆成多个小任务(Fork)
  2. 多个线程并行处理这些小任务
  3. 再把结果合并(Join)

举例:

java 复制代码
list.parallelStream().forEach(x -> doSomething(x));

执行过程:

  1. 把 list 按数据结构(如 ArrayList)拆成多段
  2. 每一段交给一个线程处理
  3. 所有线程处理完后,汇总结果

三、Stream 并行的线程池从哪里来?

👉 默认使用:ForkJoinPool.commonPool()

也就是 JVM 全局共享的公共线程池:

java 复制代码
ForkJoinPool.commonPool()

线程数默认是:

text 复制代码
CPU 核心数 - 1

可以通过参数修改:

bash 复制代码
-Djava.util.concurrent.ForkJoinPool.common.parallelism=8

⚠️ 重要点:

  • 所有 parallelStream 默认都用这个公共线程池
  • Web 项目中多个并行流会抢同一个线程池
  • 如果并行流中有阻塞操作(IO、数据库),容易拖垮整个池

四、并行 Stream 的执行流程(完整)

  1. 创建 Stream

    java 复制代码
    list.parallelStream()
  2. 构建中间操作链

    java 复制代码
    .map(...)
    .filter(...)
  3. 触发终止操作

    java 复制代码
    .forEach(...)
  4. ForkJoinPool 将数据拆分成多个任务

  5. 多个工作线程并行执行 Lambda

  6. 合并最终结果

👉 中间操作是惰性执行的

👉 终止操作才真正触发并行计算


五、并行流适用场景

适合:

  • CPU 密集型任务
  • 大数据量计算
  • 无共享变量 / 无锁逻辑

不适合:

  • IO 操作(查库、远程调用)
  • 需要保证顺序(forEachOrdered 才能保证)
  • 修改共享变量

错误示例:

java 复制代码
int sum = 0;
list.parallelStream().forEach(x -> sum += x); // ❌线程不安全

正确示例:

java 复制代码
int sum = list.parallelStream().mapToInt(i -> i).sum();

六、面试标准回答版(推荐背)

Stream 并行流底层基于 Fork/Join 框架实现,通过分治思想把数据拆分成多个子任务并行执行。

默认使用的是 ForkJoinPool.commonPool 作为线程池,线程数通常为 CPU 核心数减 1。

Stream 采用惰性执行机制,只有在终止操作时才会真正触发并行计算。

并行流适合 CPU 密集型任务,不适合包含 IO 或共享状态的场景,否则可能导致线程竞争或性能下降。


什么是ForkJoin框架 适用场景
虽然目前处理器核心数已经发展到很大数目,但是按任务并发处理并不能完全充分的利用处理器资源,
因为一般的应用程序没有那么多的并发处理任务。基于这种现状,考虑把一个任务拆分成多个单元,每
个单元分别得到执行,最后合并每个单元的结果。
Fork/Join框架是JAVA7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干小任务,最终
汇总每个小任务结果得到大任务结果的框架

很好,这一题在 Java 并发 / Stream 并行流 / 多核利用 里非常核心,我们系统讲一下:


一、什么是 Fork/Join 框架?

Fork/Join 框架是 Java 7 引入的一种并行计算框架,核心思想是:

👉 把一个大任务拆分成多个小任务并行执行(Fork),
再把多个小任务的结果合并(Join)得到最终结果。

一句话总结:

分而治之(Divide & Conquer)并行框架。


二、Fork/Join 的工作流程(核心思想)

  1. 接到一个大任务

  2. 判断任务是否足够小

    • 如果足够小 → 直接计算
    • 如果不够小 → 拆分成两个或多个子任务(fork)
  3. 子任务并行执行

  4. 等待子任务结果(join)

  5. 合并结果返回

伪流程:

text 复制代码
大任务
  ↓ fork
子任务1    子任务2
   ↓         ↓
  计算      计算
     ← join →
    合并结果

三、Fork/Join 的核心组件

1️⃣ ForkJoinPool(线程池)

专门为 Fork/Join 设计的线程池

特点:

  • 每个线程有自己的 任务队列
  • 采用 工作窃取(Work-Stealing)算法

2️⃣ RecursiveTask / RecursiveAction

用于定义任务:

是否有返回值
RecursiveTask 有返回值
RecursiveAction 无返回值

例如:计算 1 到 100 的和

java 复制代码
class SumTask extends RecursiveTask<Integer> {
    private int start, end;

    public SumTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    protected Integer compute() {
        if (end - start <= 10) { // 小任务
            int sum = 0;
            for (int i = start; i <= end; i++) sum += i;
            return sum;
        }
        int mid = (start + end) / 2;
        SumTask left = new SumTask(start, mid);
        SumTask right = new SumTask(mid + 1, end);

        left.fork();              // 拆
        int rightResult = right.compute(); // 当前线程算一半
        int leftResult = left.join();      // 等另一半

        return leftResult + rightResult;
    }
}

四、Fork/Join 的核心机制:工作窃取(Work-Stealing)

每个线程有自己的双端队列:

  • 自己队列:从队头取任务
  • 空闲线程:从别人的队尾偷任务

好处:

✅ 负载均衡

✅ 尽量让所有 CPU 核心忙起来

✅ 减少线程切换


五、Fork/Join 适用场景

✅ 适合的场景

特点:

  1. 任务可以拆分成多个子任务
  2. 子任务之间 相互独立
  3. 计算型任务(CPU 密集型)
  4. 每个子任务执行时间差不多

典型应用场景:

✔ 大数组求和

✔ 大文件统计单词数

✔ 大集合批量计算

✔ 图像处理(分块处理)

✔ 并行 Stream(parallelStream)

✔ 快速排序 / 归并排序

✔ 递归型任务


❌ 不适合的场景

🚫 IO 密集型任务(数据库、网络)

🚫 任务不能拆分

🚫 子任务之间强依赖

🚫 任务非常小(拆分成本大于收益)

🚫 需要顺序执行的业务逻辑


六、Fork/Join 与普通线程池的区别

对比项 普通线程池 ForkJoinPool
任务模型 一个任务一个线程 任务递归拆分
调度方式 FIFO 队列 工作窃取
适合场景 IO型 / 独立任务 计算型 / 可拆分任务
CPU 利用率 一般

七、Fork/Join 和 Stream 并行流的关系

parallelStream() 底层就是用的:

👉 ForkJoinPool.commonPool()

也就是说:

java 复制代码
list.parallelStream().forEach(...)

本质上是:

  • 把集合拆分成多个子任务
  • 交给 ForkJoinPool 并行执行
  • 最后合并结果

八、一句话总结(面试用)

Fork/Join 是 Java 提供的一种并行计算框架,

通过"分而治之"的方式把大任务拆成多个小任务并行执行,

使用工作窃取算法提升 CPU 利用率,

适用于可拆分的计算密集型任务,如并行流、排序、批量计算等。


Java种的代理有几种实现方式?

动态代理

JDK >>> Proxy

1 面向接口的动态代理 代理一个对象去增强面向某个接口中定义的方法

2 没有接口不可用

3 只能读取到接口上的一些注解

MyBatis

DeptMapper dm=sqlSession.getMapper(DeptMapper.class)

第三方 CGlib

1 面向父类的动态代理

2 有没有接口都可以使用

3 可以读取类上的注解

AOP 日志 性能检测 事务

MyBatis 源码 spring源码


一、Java 中代理的实现方式有几种?

两种主流方式:

  1. JDK 动态代理(Proxy)
  2. CGLIB 动态代理(第三方字节码技术)

(早期:静态代理 → 编译期写死,一般不重点考)


二、JDK 动态代理(基于接口)

原理

通过 java.lang.reflect.Proxy + InvocationHandler

在运行期生成一个 实现接口的代理类


特点

  1. 👉 面向接口的代理
  2. 👉 被代理对象 必须有接口
  3. 👉 生成的代理类:实现接口,而不是继承类
  4. 👉 只能拿到接口层面的方法和注解
  5. 👉 反射调用真实方法

使用场景

  • AOP(基于接口)
  • MyBatis Mapper 代理

例如:

java 复制代码
DeptMapper dm = sqlSession.getMapper(DeptMapper.class);

本质:

MyBatis 用 JDK 动态代理,给 DeptMapper 接口生成代理对象

调用方法 → 拦截 → 执行 SQL


适用总结

✔ 有接口

✔ 轻量

✔ JDK 原生支持

❌ 没有接口就用不了


三、CGLIB 动态代理(基于继承)

原理

通过 ASM 字节码技术,在运行期生成一个 子类 来代理目标类

本质:继承目标类 + 方法增强


特点

  1. 👉 面向父类(继承)的代理
  2. 👉 有没有接口都能用
  3. 👉 能代理普通类
  4. 👉 可以读取到类上的注解
  5. 👉 不能代理 final 类和 final 方法

使用场景

  • Spring AOP(当没有接口时)
  • 事务管理
  • 日志增强
  • 性能监控

四、JDK 动态代理 vs CGLIB 动态代理

对比项 JDK 动态代理 CGLIB 动态代理
代理对象 接口
是否需要接口 必须 不需要
底层原理 反射 字节码生成子类
能否代理普通类
是否能代理 final 类 ❌(无意义)
是否能获取类注解
Spring 默认选择 有接口用 JDK 无接口用 CGLIB

五、和 AOP、MyBatis 的关系

MyBatis

java 复制代码
DeptMapper mapper = sqlSession.getMapper(DeptMapper.class);

👉 本质:JDK 动态代理接口


Spring AOP

  • 如果目标类有接口 → 用 JDK 动态代理
  • 如果目标类没接口 → 用 CGLIB

例如:

java 复制代码
@Transactional
public void save(){}

Spring 底层就是通过代理对象增强方法(事务)


六、标准面试回答(可背)

Java 中代理主要有两种实现方式:JDK 动态代理和 CGLIB 动态代理。

JDK 动态代理是基于接口的代理,必须有接口,通过 Proxy 在运行期生成实现接口的代理类,常用于 MyBatis 的 Mapper 代理。

CGLIB 动态代理是基于继承的代理,通过生成目标类的子类来实现增强,没有接口也可以使用,常用于 Spring AOP 中的事务、日志、性能监控。

Spring 默认:有接口用 JDK,没有接口用 CGLIB。


Condition 类和Object 类锁方法区别
\1. Condition 类的 awiat 方法和 Object 类的 wait 方法等效
\2. Condition 类的 signal 方法和 Object 类的 notify 方法等效
\3. Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效
\4. ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的


一、对应关系(你写的这几条是正确的)

  1. Condition.await()Object.wait()
  2. Condition.signal()Object.notify()
  3. Condition.signalAll()Object.notifyAll()

它们本质作用都是:

👉 让当前线程进入等待队列

👉 被唤醒后继续竞争锁


二、最大的本质区别:依赖的锁不同

Object 的 wait/notify

  • 依赖:synchronized 内置锁
  • 必须在 synchronized(obj) 中调用
java 复制代码
synchronized (obj) {
    obj.wait();
}

否则直接抛异常:

IllegalMonitorStateException


Condition 的 await/signal

  • 依赖:ReentrantLock 显式锁
  • 必须先 lock() 再用
java 复制代码
lock.lock();
try {
    condition.await();
} finally {
    lock.unlock();
}

三、功能层面的关键区别(重点)

1️⃣ Object 只能有一个等待队列

java 复制代码
synchronized(obj) {
    obj.wait();
}

一个 obj 只有 一个等待队列

👉 notify() 只能随机唤醒其中一个线程

👉 不能区分线程用途


2️⃣ Condition 可以有多个等待队列(重点优势)

java 复制代码
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();

一个 ReentrantLock 可以创建多个 Condition

  • 等待生产条件 → c1
  • 等待消费条件 → c2

可以:

java 复制代码
c1.signal(); // 只唤醒生产者
c2.signal(); // 只唤醒消费者

👉 这就是你第 4 点说的:

ReentrantLock 可以唤醒指定条件的线程,而 Object 的唤醒是随机的

这是 Condition 最大的价值


四、使用方式对比总结

对比点 Object(wait/notify) Condition(await/signal)
依赖锁 synchronized ReentrantLock
等待队列数量 1 个 可以多个
是否可指定唤醒 ❌ 不行 ✅ 可以
灵活性
适合场景 简单同步 复杂并发控制

五、典型应用场景

Object 方式(老写法)

java 复制代码
synchronized(queue) {
    while(queue.isEmpty()) {
        queue.wait();
    }
}

Condition 方式(高并发推荐)

java 复制代码
lock.lock();
try {
    while(queue.isEmpty()) {
        notEmpty.await();
    }
} finally {
    lock.unlock();
}

可区分:

  • notEmpty
  • notFull

👉 生产者/消费者模型更优雅


六、面试标准回答(可背)

Condition 和 Object 的 wait/notify 作用类似,

await 等价于 wait,signal 等价于 notify,signalAll 等价于 notifyAll。

不同点在于:

Object 的等待队列只有一个,notify 唤醒的是随机线程;

而 Condition 依赖 ReentrantLock,可以创建多个条件队列,从而实现对不同条件线程的精确唤醒。

Condition 相比 wait/notify 更灵活,适用于复杂并发场景。


equals()和==区别,为什么重写equal要重写
hashcode?
==是运算符 equals()是一个来自于Object的方法
==可以用于基本数据类型和引用
equals只能用于引用类型
== 两端如果是基本数据类型,就是判断值是否相等
equals()再重写之后就是判断两个对象的属性值是否相等
equals如果不重写就是 ==
重写equals可以让我们判断两个对象是否相同
Object中定义的hashcode方法生成的哈希码能保证同一个类的对象的哈希码一定是不同的
当equals 返回为true,我们在逻辑上可以认为是同一个对象,但是查看哈希码,发现哈希码不同,和
equals方法的返回值结果违背
Object中定义的hashcode方法生成的哈希码跟对象的本身属性值是无关的
重写hashCode之后我们可以自定义hash值的生成规则,可以通过对象的属性值计算出hash码
HashMap中,借助equals和hashcode方法来完成数据的存储
将根据对象的内容查询转换为索引的查询


一、==equals() 的区别

1️⃣ 本质不同

  • ==运算符
  • equals()Object 类的方法

2️⃣ 作用对象不同

  • ==

    • 可用于 基本类型引用类型
  • equals()

    • 只能用于 引用类型

3️⃣ 比较规则不同

(1)==
  • 如果是基本类型:
    👉 比较的是 值是否相等
  • 如果是引用类型:
    👉 比较的是 两个引用是否指向同一个对象(地址是否相同)

(2)equals()
  • 如果没有重写:
    👉 等价于 ==(比较地址)
  • 如果被重写:
    👉 一般用来比较 对象的内容是否相同(属性值)

例如:

java 复制代码
User u1 = new User(1, "Tom");
User u2 = new User(1, "Tom");

u1 == u2        // false(地址不同)
u1.equals(u2)  // true(如果按 id、name 比较)

二、为什么重写 equals() 必须重写 hashCode()

1️⃣ 先说结论(面试重点):

如果两个对象 equals() 返回 true,那么它们的 hashCode() 必须相等。

这是 Java 规定的通用约定


2️⃣ 如果只重写 equals,不重写 hashCode 会怎样?

Object 默认的 hashCode()

  • 和对象地址有关
  • 与对象属性无关

可能出现这种情况:

java 复制代码
User u1 = new User(1, "Tom");
User u2 = new User(1, "Tom");

u1.equals(u2) == true
u1.hashCode() != u2.hashCode()

👉 逻辑上是"同一个对象",

👉 哈希值却不同,产生矛盾


3️⃣ 在 HashMap / HashSet 中会出问题

HashMap 存数据时:

  1. 先用 hashCode() 定位桶
  2. 再用 equals() 判断是否同一个 key

如果:

  • equals 相同
  • hashCode 不同

那就会:

👉 被放进不同桶

👉 查不到数据

👉 出现重复 key


4️⃣ 正确做法

重写 equals() 时:

👉 必须同时重写 hashCode()

👉 用参与 equals 比较的属性来计算 hash

例如:

java 复制代码
@Override
public int hashCode() {
    return Objects.hash(id, name);
}

三、总结(面试一句话版)

== 比较的是值或引用地址,equals() 比较的是对象内容;

如果重写了 equals(),就必须重写 hashCode(),保证相等对象具有相同的哈希值,否则在 HashMap、HashSet 中会导致数据存取异常。


hashmap为什么用红黑树不用普通的AVL 树?


HashMap 选择红黑树而不是 AVL 树,是因为 红黑树更适合"插入和删除频繁"的场景,综合性能更稳定

一、AVL 树 vs 红黑树本质区别

🌳 AVL 树(强平衡)

特点:

  • 任意节点左右子树高度差 ≤ 1

  • 树非常"矮",查找效率高

  • 插入、删除时:

    • 很容易破坏平衡
    • 需要频繁旋转来恢复平衡

适合场景:

查找远多于插入和删除


🌳 红黑树(弱平衡)

特点:

  • 不要求严格高度平衡

  • 只保证:

    👉 最长路径 ≤ 最短路径的 2 倍

  • 插入、删除时:

    • 旋转次数 少于 AVL 树
    • 维护成本更低

适合场景:

插入、删除、查找都很多的综合型场景


二、为什么 HashMap 不选 AVL 树?

HashMap 中树化的桶:

  • 本质是为了:

    👉 避免极端情况下链表退化成 O(n)

  • 使用场景特点:

    • put(插入)很多
    • remove(删除)可能有
    • get(查询)也很多

👉 属于:增删查都频繁

如果用 AVL:

  • 每次插入 / 删除:

    • 可能触发多次旋转
    • 维护平衡成本高
  • 在 HashMap 这种"高频修改结构"里:

    👉 会拖慢 put/remove 性能

如果用红黑树:

  • 牺牲一点点查找高度
  • 换来:
    👉 更少的旋转
    👉 更低的维护成本
    👉 更稳定的整体性能

三、从 HashMap 的目标来看

HashMap 的目标不是:

极致查找性能

而是:

在最坏情况下保证性能不要太差(从 O(n) 变为 O(log n)),同时不影响插入效率

红黑树:

  • 查找:O(log n)
  • 插入/删除:旋转少
  • 综合性能更平衡

这正好符合 HashMap 的设计目标。


四、面试简答版(30 秒)

AVL 树是严格平衡树,查找快,但插入和删除时需要频繁旋转,维护成本高;

红黑树是弱平衡树,虽然高度略高,但插入和删除时旋转次数更少,综合性能更稳定。

HashMap 的使用场景中插入、删除和查询都很频繁,因此选择红黑树而不是 AVL 树。


hashmap线程安全的方式?


一、结论先给出

HashMap 是线程不安全的,在多线程环境下可能出现数据丢失、死循环等问题。

常见的线程安全方案有三类:

  1. Collections.synchronizedMap
  2. ConcurrentHashMap
  3. 自己加锁(synchronized / Lock)

二、方案一:Collections.synchronizedMap()

java 复制代码
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());

原理

  • 内部对所有方法加了 synchronized
  • 本质是:给 HashMap 套了一层同步代理

特点

✅ 实现简单

❌ 粒度大:

  • 整个 Map 只允许一个线程访问
  • 并发性能差

适合场景:

并发量小,对性能要求不高


三、方案二:ConcurrentHashMap(推荐)

java 复制代码
Map<String, String> map = new ConcurrentHashMap<>();

原理(JDK8)

  • 使用 CAS + synchronized(细粒度)
  • 只锁桶(链表或红黑树),不是锁整个 Map

特点

✅ 高并发性能好

✅ 线程安全

❌ 实现复杂

适合场景:

高并发读写,比如缓存、计数、在线用户表


四、方案三:自己控制锁

java 复制代码
Map<String, String> map = new HashMap<>();

synchronized(map) {
    map.put("a", "1");
}

或:

java 复制代码
Lock lock = new ReentrantLock();
lock.lock();
try {
    map.put("a", "1");
} finally {
    lock.unlock();
}

特点

  • 灵活
  • 但容易写错
  • 不如 ConcurrentHashMap 稳定

五、为什么不用 Hashtable?

Hashtable 是线程安全的,但:

  • 所有方法都加 synchronized
  • 性能很差
    👉 现在基本被 ConcurrentHashMap 取代

六、面试简答版(30 秒)

HashMap 本身是线程不安全的,可以通过 Collections.synchronizedMap 包装成线程安全的 Map,但它是对整个 Map 加锁,性能较差;更推荐使用 ConcurrentHashMap,它通过 CAS 和分段锁(JDK8 为桶级锁)实现线程安全,在高并发场景下性能更好。


hashmap在1.8中做了那些优化
在java1.7中,HashMap的数据结构为数组+单向链表。java1.8中变成数组+单向链表+红黑树
链表插入节点的方式
在java1.7中,插入链表节点使用头插法。java1.8变成尾查法
hash函数
java1.8的hash()中。将hash值高位(前16位)参与到取模的运算中,使得计算结果的不确定性增强,降低
发生哈希碰撞的概率
扩容优化:
扩容以后1.7对元素进行rehash算法,计算原来每个元素在扩容之后的哈希表中的位置1.8借助2倍扩容
机制,元素不需要进行重新计算位置
JDK1.8在扩容并没有像JDK1.7那样,重新计算每个元素的哈希值,而是通过高位运算(e.hash & oldCap)
来确定元素是否需要移动,比如 key1 的信息如下:
使用 e.hash & oldCap 得到的结果,高一位为0,当结果为0时表示元素在扩容时位置不会发生任何变
化,而 key2信息如下
高一位为 1,当结果为 1 时,表示元素在扩容时位置发生了变化,新的下标位置等于原下标位置 + 原数
组长度 hashmap,不必像1.7一样全部重新计算位置

这题是 HashMap 版本演进题,你给的素材是对的,我帮你整理成一版**面试标准答案(条理清晰版)**👇


一、整体结构优化(最重要)

JDK 1.7:

数组 + 链表

JDK 1.8:

数组 + 链表 + 红黑树

当某个桶中的链表长度过长时:

  • JDK1.8 会把链表转换成红黑树
  • 将最坏时间复杂度从 O(n) 优化为 O(log n)

二、链表插入方式改变

JDK 1.7:

  • 采用 头插法
  • 多线程下扩容可能形成环形链表(死循环)

JDK 1.8:

  • 改为 尾插法
  • 避免了扩容时链表反转导致的并发死循环问题

三、hash 算法优化

JDK 1.8 的 hash():

java 复制代码
h ^ (h >>> 16)

作用:

  • 高 16 位参与取模
  • 降低哈希冲突概率
  • 分布更均匀

四、扩容(resize)机制优化

JDK 1.7:

  • 扩容时:
    👉 对每个元素重新计算 hash 和新下标
    👉 成本高

JDK 1.8:

  • 由于容量始终是 2 的幂
  • 通过:
java 复制代码
e.hash & oldCap

判断:

  • =0 → 位置不变
  • ≠0 → 新下标 = 原下标 + oldCap

👉 不需要重新算 hash


五、树化与反树化机制

JDK1.8 引入:

  • 链表长度 ≥ 8 → 转红黑树
  • 红黑树节点 < 6 → 转回链表
  • 且数组长度 ≥ 64 才允许树化

目的:

👉 防止小表过早树化,浪费性能


六、面试简答版(60 秒)

JDK1.8 对 HashMap 做了多项优化:

1)底层结构由数组+链表升级为数组+链表+红黑树,降低极端情况下的时间复杂度;

2)链表插入方式由头插法改为尾插法,避免并发扩容形成死循环;

3)优化了 hash 算法,让高位参与运算,减少哈希冲突;

4)扩容时利用容量为 2 的幂,通过 e.hash & oldCap 判断是否移动元素,避免重新计算位置;

5)增加了树化与反树化机制,平衡空间和性能。


sleep 与 wait 区别


一、所属类不同

  • sleep()

    👉 属于 Thread 类的方法

  • wait()

    👉 属于 Object 类的方法


二、对锁的影响不同(重点)

sleep()

  • 让线程休眠指定时间

  • 不会释放锁

  • 如果在 synchronized 中调用:

    • 线程睡觉
    • 锁仍然被它占着

wait()

  • 让线程进入等待状态

  • 会释放锁

  • 需要其他线程调用:

    • notify()notifyAll()
      才能被唤醒

三、使用位置不同

sleep()

  • 可以在任何地方调用
  • 不要求必须在同步块中
java 复制代码
Thread.sleep(1000);

wait()

  • 必须在 同步代码块或同步方法中调用
  • 必须先获得对象锁
java 复制代码
synchronized(obj) {
    obj.wait();
}

否则会抛:

👉 IllegalMonitorStateException


四、唤醒方式不同

sleep()

  • 时间到 → 自动醒
  • 或被 interrupt() 打断

wait()

  • 只能靠:

    • notify()
    • notifyAll()
      唤醒(同一个对象)

五、状态不同

  • sleep()

    👉 进入 TIMED_WAITING

  • wait()

    👉 进入 WAITING 或 TIMED_WAITING


六、对比总结表

对比点 sleep() wait()
所属类 Thread Object
是否释放锁 ❌ 不释放 ✅ 释放
是否必须同步 ❌ 不需要 ✅ 必须
唤醒方式 时间到 / interrupt notify / notifyAll
使用场景 延时 线程通信

七、面试一句话版

sleep 是 Thread 的方法,只会让线程休眠但不释放锁;wait 是 Object 的方法,会释放锁并进入等待队列,需要通过 notify 或 notifyAll 唤醒,并且只能在同步代码块中调用。


synchronized 和 ReentrantLock 的区别


一、共同点

  1. 都用于控制多个线程对共享资源的访问

  2. 都是可重入锁(同一线程可多次获取同一把锁)

  3. 都能保证:

    • 互斥性
    • 可见性

二、主要区别

1️⃣ 使用方式不同

  • synchronized

    👉 隐式加锁/释放锁(JVM 自动管理)

  • ReentrantLock

    👉 显式加锁/释放锁

java 复制代码
lock.lock();
try {
   ...
} finally {
   lock.unlock();
}

2️⃣ 对中断的支持不同

  • synchronized

    ❌ 等待锁时不能响应中断

  • ReentrantLock

    ✅ 可以响应中断

java 复制代码
lock.lockInterruptibly();

3️⃣ 是否支持公平锁

  • synchronized

    ❌ 只能是非公平锁

  • ReentrantLock

    ✅ 可以选择公平锁或非公平锁

java 复制代码
new ReentrantLock(true); // 公平锁

4️⃣ 是否能尝试获取锁

  • synchronized

    ❌ 不可以

  • ReentrantLock

    ✅ 可以

java 复制代码
lock.tryLock();

5️⃣ 条件队列不同

  • synchronized

    👉 只有一个等待队列(wait/notify

  • ReentrantLock

    👉 可以创建多个 Condition

java 复制代码
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();

更适合复杂线程通信


6️⃣ 实现层级不同

  • synchronized
    👉 JVM 级别关键字
  • ReentrantLock
    👉 API 层面(JUC 包)

7️⃣ 异常时释放锁

  • synchronized

    ✅ 异常自动释放锁

  • ReentrantLock

    ❌ 必须在 finally 中手动释放,否则可能死锁


8️⃣ 功能扩展性

  • synchronized

    👉 功能固定,简单

  • ReentrantLock

    👉 功能更丰富:

    • 可中断
    • 可超时
    • 可公平
    • 多 Condition
    • 读写锁(ReentrantReadWriteLock

⚠️ 关于"悲观锁 / 乐观锁"的纠正

❌ 不严谨说法:

synchronized 是悲观锁,Lock 是乐观锁

✅ 正确说法:

  • ReentrantLocksynchronized 本质都是悲观锁(互斥)
  • ReentrantLock 内部部分实现用了 CAS(优化手段)
  • 真正的乐观锁:AtomicInteger、CAS

三、面试一句话总结(推荐)

synchronized 是 JVM 层面的内置锁,使用简单,自动释放;ReentrantLock 是 JUC 提供的显式锁,功能更丰富,支持公平锁、可中断、可尝试获取锁和多个条件队列,但需要手动释放锁。


Tomcat为什么要重写类加载器? 这里简单解释类加载器双亲委派: 无法实现隔离性:如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的 类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。一个web容器可能要部署两个 或者多个应用程序,不同的应用程序,可能会依赖同一个第三方类库的不同版本,因此要保证每一个应 用程序的类库都是独立、相互隔离的。部署在同一个web容器中的相同类库的相同版本可以共享,否 则,会有重复的类库被加载进JVM, web容器也有自己的类库,不能和应用程序的类库混淆,需要相互隔 离 无法实现热替换:jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接 取方法区中已经存在的,修改后的jsp是不会重新加载的。 打破双亲委派机制(参照JVM中的内容)OSGI是基于Java语言的动态模块化规范,类加载器之间是网状结 构,更加灵活,但是也更复杂,JNDI服务,使用线程上线文类加载器,父类加载器去使用子类加载器 2. tomcat自己定义的类加载器: CommonClassLoader:tomcat最基本的类加载器,加载路径中的class可以被tomcat和各个webapp访 问 CatalinaClassLoader:tomcat私有的类加载器,webapp不能访问其加载路径下的class,即对webapp 不可见 SharedClassLoader:各个webapp共享的类加载器,对tomcat不可见 WebappClassLoader:webapp私有的类加载器,只对当前webapp可见 3. 每一个web应用程序对应一个WebappClassLoader,每一个jsp文件对应一个JspClassLoader,所 以这两个类加载器有多个实例 4. 工作原理: a. CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而 实现了公有类库的共用 b. CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离 c. WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之 间相互隔离,多个WebAppClassLoader是同级关系 d. 而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为 了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立 一个新的Jsp类加载器来实现JSP文件的HotSwap功能 5. tomcat目录结构,与上面的类加载器对应 /common/* /server/* /shared/* /WEB-INF/* 6. 默认情况下,conf目录下的catalina.properties文件,没有指定server.loader以及 shared.loader,所以tomcat没有建立CatalinaClassLoader和SharedClassLoader的实例,这两个 都会使用CommonClassLoader来代替。Tomcat6之后,把common、shared、server目录合成 了一个lib目录。所以在我们的服务器里看不到common、shared、server目录。

一、为什么不能直接用 JVM 默认的类加载机制?

Tomcat 重写类加载器,是为了实现:多应用隔离 + 共享公共类库 + 支持热部署/热替换,并部分打破双亲委派。

1️⃣ 无法实现 Web 应用之间的隔离

JVM 默认双亲委派:

👉 类只看全限定名,不看版本

如果两个 Web 应用:

  • A 用 mysql-connector 5.x
  • B 用 mysql-connector 8.x

默认类加载机制下:

❌ JVM 只能加载一个 com.mysql.Driver

❌ 版本冲突

❌ 应用互相影响

Tomcat 需要做到:

  • 每个 Web 应用的类库 互不影响
  • 同版本库可以共享
  • 不同版本库必须隔离

👉 默认类加载器做不到


2️⃣ 无法实现热部署 / JSP 热更新

JSP 会被编译成 .class

但 JVM 规则是:

同一个类加载器 + 同一个类名 = 只能加载一次

如果 JSP 改了:

  • 类名不变
  • 类加载器不变
    👉 JVM 认为:老类还在
    ❌ 新 JSP 不会生效

Tomcat 的解决方案:

👉 换类加载器实例,而不是换类


3️⃣ 需要"打破双亲委派"

有些场景:

  • 父加载器需要用子加载器加载的类
    例如:
  • JNDI
  • SPI
  • OSGi
  • JDBC Driver

Tomcat 使用:

👉 线程上下文类加载器 (TCCL)

👉 实现"父用子"的效果


二、Tomcat 自定义的类加载器体系

四层结构(逻辑上):

复制代码
Bootstrap
   ↓
CommonClassLoader
   ↓
---------------------------------
| CatalinaClassLoader  | SharedClassLoader |
---------------------------------
              ↓
      WebAppClassLoader (每个应用一个)
              ↓
         JspClassLoader (每个 JSP 一个)

各加载器职责

✅ CommonClassLoader
  • 加载:公共依赖
  • 对象:Tomcat + 所有 WebApp 都能用
  • 对应:lib

✅ CatalinaClassLoader
  • Tomcat 自己用
  • Web 应用不可见
  • 用于 Tomcat 内部类

✅ SharedClassLoader
  • 各 WebApp 共享
  • Tomcat 不可见
  • 放公共业务库

✅ WebappClassLoader(最重要)
  • 每个 Web 应用一个
  • 只对当前应用可见
  • 实现应用隔离

✅ JspClassLoader
  • 每个 JSP 一个
  • JSP 修改 → 丢弃旧加载器 → 新建一个
  • 实现 JSP 热替换

三、Tomcat 类加载规则(重点)

1️⃣ Web 应用优先加载自己

不同于 JVM 默认:

Tomcat:先自己找 → 再找父类加载器

目的是:

  • 避免 WebApp 被 Tomcat 公共库"覆盖"
  • 实现版本隔离

2️⃣ WebApp 之间互相隔离

  • 每个 WebApp 有自己的 WebappClassLoader
  • 它们是:
    👉 兄弟关系,不是父子关系

所以:

  • A 加载的类
  • B 看不到

3️⃣ JSP 热更新原理

JSP 修改后:

  1. 丢弃原 JspClassLoader
  2. 新建一个 JspClassLoader
  3. 加载新编译的 class

👉 JVM 看来是:

新类加载器 + 同名类 = 新类

所以能生效


四、目录结构对应关系(老版本)

目录 加载器
/common CommonClassLoader
/server CatalinaClassLoader
/shared SharedClassLoader
/WEB-INF WebappClassLoader

Tomcat6 以后:

👉 合并成 /lib


五、总结(面试标准版)

Tomcat 重写类加载器,是为了解决 JVM 默认类加载机制无法实现多 Web 应用隔离、类库版本冲突以及热部署的问题。

它为每个 Web 应用创建独立的 WebappClassLoader,使应用之间相互隔离;通过 JspClassLoader 实现 JSP 热更新;同时在部分类加载场景中打破双亲委派机制,以支持 SPI、JNDI 等需求。


tryLock和Lock和lockInterruptibly 的区别
\1. tryLock 能获得锁就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnit unit),可以
增加时间限制,如果超过该时间段还没获得锁,返回 false
\2. lock 能获得锁就返回 true,不能的话一直等待获得锁
\3. lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程, lock 不会
抛出异常,而 lockInterruptibly 会抛出异常。


一、lock()

特点:一直等锁,不能被中断

java 复制代码
lock.lock();

含义是:

  • 如果锁空闲 → 立刻获得锁
  • 如果锁被占用 → 当前线程一直阻塞等待
  • 即使你对线程调用了 interrupt()
    👉 也不会抛异常,也不会停止等待

总结一句话:

lock() = 死等锁,不响应中断


二、tryLock()

特点:不等 or 限时等

1️⃣ 无参版本

java 复制代码
lock.tryLock();
  • 能拿到锁 → 返回 true
  • 拿不到锁 → 立刻返回 false
  • 不阻塞

适合:

👉 避免死等锁

👉 实现"能干就干,不能就走"的逻辑


2️⃣ 带超时版本

java 复制代码
lock.tryLock(3, TimeUnit.SECONDS);
  • 最多等 3 秒
  • 期间如果拿到锁 → 返回 true
  • 超过时间还没拿到 → 返回 false
  • 等待期间:可以被中断(会抛异常)

三、lockInterruptibly()

特点:可以被中断的 lock

java 复制代码
lock.lockInterruptibly();

行为:

  • 能拿到锁 → 正常执行
  • 拿不到锁 → 阻塞等待
  • 如果在等待过程中被 interrupt()
    👉 InterruptedException
    👉 结束等待锁

对比:

方法 会不会等锁 能不能被中断
lock() 会,一直等 ❌ 不响应
tryLock() 不等 / 限时等 ✅(限时版可中断)
lockInterruptibly() 会等 ✅ 可中断

四、结合你说的这句话

lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程, lock 不会抛出异常,而 lockInterruptibly 会抛出异常。

✔ 这句话是对的

但可以说得更严谨一点:

当线程在等待锁时,如果调用 interrupt()

  • 使用 lock() 的线程仍然会继续等待
  • 使用 lockInterruptibly() 的线程会抛出 InterruptedException 并结束等待

五、经典面试一句话版本

你可以这样背:

lock() 会一直阻塞获取锁,不响应中断;
tryLock() 立即尝试获取锁,获取不到直接返回 false,也可以设置超时时间;
lockInterruptibly() 在等待锁的过程中可以响应中断并抛出异常。


简述一下Java运行时数据区? Java虚拟机栈 与程序计数器一样,Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与 线程相同。 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,是方法运行时的基础数据结构 程序计数器 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字 节码的行号指示器。 由于 Java 虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定 的时刻,一个处理器内核都只会执行一条线程中的指令。 本地方法栈 本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是虚 拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法 服务。Sun HotSpot 虚拟机直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区 域也会抛出 StackOverflowError 和 OutOfMemoryError 异常 Java堆 对于大多数应用来说,Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。Java 堆是被所 有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所 有的对象实例都在这里分配内存。 方法区 方法区(Method Area)与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类 信息、常量、静态变量、即时编译器编译后的代码等数据


一、线程私有区域

1️⃣ 程序计数器(PC Register)

  • 作用:记录当前线程正在执行的 字节码指令地址

  • 用途:线程切换后能恢复到正确执行位置

  • 特点:

    • 每个线程都有一个
    • 不会发生 OOM

2️⃣ Java 虚拟机栈(JVM Stack)

  • 作用:存放 方法执行时的栈帧

  • 栈帧包含:

    • 局部变量表
    • 操作数栈
    • 动态链接
    • 方法出口
  • 特点:

    • 线程私有
    • 方法调用 → 入栈,方法结束 → 出栈
  • 可能异常:

    • StackOverflowError(栈太深)
    • OutOfMemoryError(栈扩展失败)

3️⃣ 本地方法栈(Native Method Stack)

  • 作用:为 **Native 方法(C/C++)**服务

  • HotSpot 中与虚拟机栈合并实现

  • 可能异常:

    • StackOverflowError
    • OutOfMemoryError

二、线程共享区域

4️⃣ Java 堆(Heap)

  • 作用:存放 对象实例

  • 特点:

    • 所有线程共享
    • 是 JVM 中最大的一块内存
    • GC 主要工作区域
  • 通常分为:

    • 新生代(Eden、Survivor)
    • 老年代

5️⃣ 方法区(Method Area)

  • 作用:存放:

    • 类信息(类结构、字段、方法)
    • 常量
    • 静态变量
    • JIT 编译后的代码
  • 特点:

    • 线程共享
  • 在 HotSpot 中的实现:

    • JDK7 之前:永久代(PermGen)
    • JDK8 之后:元空间(Metaspace,使用本地内存)

三、总结版(面试背诵)

Java 运行时数据区包括:

程序计数器、虚拟机栈、本地方法栈、堆、方法区。

其中程序计数器、虚拟机栈、本地方法栈是线程私有的;

堆和方法区是线程共享的。

虚拟机栈负责方法执行,堆负责对象存储,方法区存放类元数据。


解决hash冲突的方式有哪些?

1开放定址法
所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列
地址总能找到,并将记录存入
2再哈希法:
再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,....,等哈希函
数计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。
3链地址法
链地址法的基本思想是:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个
单向链表,被分配到同一个索引上的多个节点可以用这个单向 链表连接起来
4建立公共溢出区
这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填
入溢出表

常见解决 Hash 冲突的方式有四种:


✅ 1. 开放定址法(Open Addressing)

思想:

发生冲突后,不存到链表,而是继续找下一个空位置

常见方式:

  • 线性探测:index+1、index+2...
  • 二次探测:+1², -1², +2²...

优点:

  • 不需要额外链表结构
  • 空间利用率高

缺点:

  • 容易产生"聚集"
  • 查找效率下降

✅ 2. 再哈希法(双重哈希)

思想:

准备多个 hash 函数:

  • 第一个冲突 → 用第二个
  • 再冲突 → 用第三个...

优点:

  • 冲突概率低
  • 不容易聚集

缺点:

  • 计算 hash 次数多
  • 性能开销更大

✅ 3. 链地址法(拉链法)⭐(HashMap 用的)

思想:

同一个下标的数据:

👉 用链表(或红黑树)串起来

复制代码
数组[index] → Node → Node → Node

JDK1.8:

  • 链表长度 > 8 → 转红黑树

优点:

  • 实现简单
  • 扩展性好
  • 不容易满表

缺点:

  • 冲突多时,链表会变长(性能下降)

✅ 4. 公共溢出区

思想:

  • 主表放正常数据
  • 冲突的统一放到"溢出表"

优点:

  • 结构简单

缺点:

  • 查找时要查两个表
  • 实际使用较少

面试一句话总结版

解决哈希冲突的常见方法包括:
开放定址法、再哈希法、链地址法以及公共溢出区法。

其中 Java 的 HashMap 采用的是链地址法,在 JDK1.8 之后,当链表过长会转换为红黑树来提高查询效率。


说一下反射,反射会影响性能吗?
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个
对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为
java语言的反射机制。反射这种运行时动态的功能可以说是非常重要的,可以说无反射不框架!!!,反射方式
实例化对象和,属性赋值和调用方法肯定比直接的慢,但是程序运行的快慢原因有很多,不能主要归于反射,
如果你只是偶尔调用一下反射,反射的影响可以忽略不计,如果你需要大量调用反射,会产生一些影响,适
当考虑减少使用或者使用缓存,你的编程的思想才是限制你程序性能的最主要的因素


一、什么是反射?

反射是指:在运行时动态获取类的信息,并操作类的属性和方法。

具体包括:

  • 获取类的:

    • 成员变量
    • 方法
    • 构造器
  • 动态:

    • 创建对象
    • 调用方法
    • 修改属性值

一句话总结:

反射机制允许程序在运行时检查和操作任意对象和类,而不需要在编译期就确定类型。


二、反射有什么用?(为什么"无反射不框架")

反射常用于:

  • Spring(IOC、AOP)
  • MyBatis(Mapper 代理)
  • 注解解析
  • 动态代理

比如:

  • 根据类名创建对象
  • 根据注解自动注入属性
  • 运行时调用方法

👉 框架的"通用性"和"解耦"基本都靠反射实现。


三、反射会影响性能吗?

结论先给你:

会比直接调用慢,但是否成为瓶颈要看使用频率。

原因:

  • 反射:

    • 需要做安全检查
    • 需要方法查找
    • 不能被 JVM 充分优化
  • 直接调用:

    • 编译期已确定
    • 可以被 JIT 优化

所以:

  • ✅ 偶尔用一次反射 → 性能影响可以忽略
  • ❌ 高频、大量反射调用 → 会有明显性能开销

四、如何降低反射带来的性能影响?

常见优化方式:

  • 缓存 Method / Field / Constructor
  • 避免在循环中频繁反射
  • 只在初始化阶段用反射

五、面试标准回答版(可背)

Java 反射是指在运行时动态获取类的结构信息,并操作对象的方法和属性。它使得程序在编译期不依赖具体类型,提高了灵活性,是很多框架如 Spring、MyBatis 的基础。

反射相比直接调用方法在性能上会有一定损耗,因为涉及方法查找和安全检查,但如果只是少量使用,性能影响可以忽略;如果高频使用反射,需要通过缓存反射结果等方式进行优化。


为什么hashmap扩容的时候是两倍?
查看源代码
在存入元素时,放入元素位置有一个 (n-1)&hash 的一个算法,和hash&(newCap-1),这里用到了一个&位运
算符
当HashMap的容量是16时,它的二进制是10000,(n-1)的二进制是01111,与hash值得计算结果如下
下面就来看一下HashMap的容量不是2的n次幂的情况,当容量为10时,二进制为01010,(n-1)的二进
制是01001,向里面添加同样的元素,结果为
可以看出,有三个不同的元素进过&运算得出了同样的结果,严重的hash碰撞了


一、HashMap 下标计算方式

HashMap 放元素的位置是这样算的:

java 复制代码
index = (n - 1) & hash

其中:

  • n = 当前数组长度(table.length)
  • hash = key 的 hash 值

这是用 位运算代替取模运算 %,速度更快。


二、为什么容量必须是 2 的幂?

如果 n = 2^k,比如:

n 二进制 n-1(二进制)
16 10000 01111
32 100000 011111

这样 (n-1) 就是:

👉 低位全是 1,高位全是 0

hash& 运算时:

复制代码
(n-1) & hash
= 只取 hash 的低 k 位

这相当于:

复制代码
hash % n

效果一致,但更快

而且:

✅ 每一位都能参与运算

✅ 分布更均匀

✅ 冲突更少


三、如果容量不是 2 的幂会怎样?

比如容量 = 10:

复制代码
n = 10      -> 1010
n - 1 = 9   -> 1001

再看 (n - 1) & hash

复制代码
1001
& hash

问题来了:

  • 中间某些位永远是 0
  • 会导致很多 hash 结果映射到同一个桶

结果就是:

👉 大量 hash 冲突

👉 链表/红黑树变长

👉 查找效率下降

你说的这个例子本质就是在说明这一点:
不是 2 的幂时,位与运算无法均匀散列。


四、那为啥"扩容时是 2 倍"?

因为:

  1. 保证新容量仍是 2 的幂
  2. 扩容时可以利用一个重要性质:

旧容量 = oldCap

新容量 = newCap = oldCap * 2

元素要么:

  • 位置不变
  • 要么:index + oldCap

判断条件只需要看 hash 的某一位是不是 1

java 复制代码
(hash & oldCap) == 0

这使得扩容时:

  • ❌ 不需要重新算 hash
  • ❌ 不需要取模
  • ✅ 只用一次位运算就能判断新位置

这就是 JDK8 扩容能这么快的原因之一。


五、总结一句话(面试版)

你可以这样答:

HashMap 扩容为 2 倍,是为了保证容量始终是 2 的幂,从而配合 (n-1) & hash 的下标计算方式,用位运算代替取模,提高性能并减少 hash 冲突。同时在扩容时,元素要么原位置不变,要么移动到原位置加旧容量的位置,迁移成本更低。


相关推荐
♡喜欢做梦17 小时前
Spring Boot 日志实战:级别、持久化与 SLF4J 配置全指南
java·spring boot·后端·spring·java-ee·log4j
QQ24391972 天前
语言在线考试与学习交流网页平台信息管理系统源码-SpringBoot后端+Vue前端+MySQL【可直接运行】
前端·spring boot·sql·学习·java-ee
想不明白的过度思考者2 天前
JavaEE进阶 ——【SpringBoot 快速上手】从环境搭建到HelloWorld实战
java·spring boot·spring·java-ee
Dylan的码园4 天前
从软件工程师看计算机是如何工作的
java·jvm·windows·java-ee
弹简特5 天前
【JavaEE09-后端部分】SpringMVC04-SpringMVC第三大核心-处理响应和@RequestMapping详解
java·spring boot·spring·java-ee·tomcat
弹简特7 天前
【JavaEE08-后端部分】SpringMVC03-SpringMVC第二大核心处理请求之Cookie/Session和获取header
java·spring boot·spring·java-ee
手握风云-8 天前
JavaEE 进阶第十九期:MyBatis-Plus,让 CRUD 飞起来
java·java-ee·mybatis
cheems95279 天前
【javaEE】TCP协议总结
网络·tcp/ip·java-ee
我命由我123459 天前
Kotlin 面向对象 - 匿名内部类、匿名内部类简化
android·java·开发语言·java-ee·kotlin·android studio·android jetpack