Java:==、equals() 与 hashCode()(HashMap)
我的理解偏差
-
把
==只理解成“比较引用地址”- 这只适用于引用类型。
- 对基本数据类型,
==比较的是值。 - 对引用类型,
==判断两个引用是否指向同一个对象。
-
把
equals()说成“判断是不是同一个对象”- 更准确的说法是:
equals()用于判断两个对象是否“逻辑相等”。 - 两个不同的对象,也完全可以因为业务字段相同而
equals() == true。
- 更准确的说法是:
-
把
hashCode()理解成可以直接定位到某个对象hashCode()主要用于散列查找,帮助 HashMap 快速缩小查找范围、定位桶。- 不同对象可能拥有相同的 hashCode,因此 hashCode 不能唯一确定一个对象。
- HashMap 找到桶后,还需要通过
equals()进一步确认 key。
-
“重写 equals 必须重写 hashCode”的关键原因没有说完整
- Java 约定:如果
a.equals(b) == true,则必须保证a.hashCode() == b.hashCode()。 - 在 HashMap 中,如果两个逻辑相等的 key 因为 hashCode 不同被定位到不同的桶,那么查找时连
equals()都可能没有机会执行,最终导致本应相等的 key 查找失败。
- Java 约定:如果
核心记忆
hashCode()负责“找桶”,equals()负责“认人”。
如果
equals()相等但hashCode()不同,两者可能被放进不同的桶,甚至没有机会进行equals()比较。
Java:ArrayList 扩容机制与 ArrayList / LinkedList 适用场景
我的理解偏差
-
把 ArrayList 的“首次扩容为 10”说成初始化时就有 10 个空间
- 使用默认构造器
new ArrayList<>()时,底层一开始通常是空数组。 - 第一次真正添加元素时,才会把容量扩到默认容量 10。
- 使用默认构造器
-
把 ArrayList 扩容理解成“只有装满后才固定扩 1.5 倍”
- 常见情况下,新容量为:
oldCapacity + (oldCapacity >> 1),也就是约 1.5 倍。 - 但本质上是“当前容量不足以容纳新元素时”触发扩容;如果一次性需要的容量比 1.5 倍还大,会直接扩到能够满足需求的容量。
- 扩容后会创建新数组,并把旧数组元素复制过去。
- 常见情况下,新容量为:
-
把 LinkedList 简单理解成“经常增删就应该用 LinkedList”
- LinkedList 在“已经定位到节点”的前提下,插入和删除本身可以做到 O(1)。
- 但如果要先按下标找到目标位置,查找过程是 O(n),因此“查找 + 增删”整体仍可能是 O(n)。
- ArrayList 在尾部追加元素通常是均摊 O(1),并不是所有新增操作都很慢。
-
没有强调 ArrayList 最大的优势是随机访问和缓存友好性
- ArrayList 底层是连续数组,按下标访问是 O(1),CPU 缓存局部性也更好。
- LinkedList 按下标访问需要从头或尾逐个节点寻找,随机访问是 O(n),而且每个节点还要额外保存前后指针。
核心记忆
ArrayList:查得快、尾部追加也快;中间插删需要搬元素。
LinkedList:节点插删本身快,但“先找到这个节点”往往不快。
不要死记“增删多用 LinkedList”;很多实际场景中,即使存在增删,ArrayList 仍然可能更合适。
Java / MySQL:乐观锁与悲观锁
我的理解偏差
-
把
SELECT ... FOR UPDATE理解成“锁住该行后,其他事务什么都不能做”SELECT ... FOR UPDATE会对目标记录加悲观锁,阻止其他事务进行冲突性的加锁或修改操作。- 但普通
SELECT在 InnoDB 的 MVCC 机制下,很多情况下仍然可以读取旧版本数据,并不一定会被阻塞。
-
把 version 乐观锁理解成“发现版本不一致后,数据库会不断重试直到成功”
version字段的核心作用是“检测并发冲突”,而不是自动重试。- 更新时通常使用:
WHERE version = oldVersion,并同时执行version = version + 1。 - 如果受影响行数为 0,说明数据已经被其他事务修改;是否重新查询并重试,由业务代码决定。
-
把 CAS 描述成“CPU 支持,所以不会出岔子”
- 更准确地说,CAS 的“比较并交换”本身是原子操作,通常由 CPU 原子指令保证。
- CAS 并不代表所有并发问题都会自动消失,例如还存在 ABA、自旋开销等问题。
核心记忆
悲观锁:先锁,再操作。
乐观锁:先操作,提交修改时再检查数据有没有被别人改过。
Java:
synchronized体现悲观锁思想;AtomicInteger的 CAS 体现乐观并发思想。
MySQL:
SELECT ... FOR UPDATE是典型悲观锁;version字段是常见乐观锁实现。
乐观锁 ≠ 自动重试。version 负责发现冲突,是否重试由程序决定。
Redis:分布式锁的加锁与解锁
我的理解偏差
-
把分布式锁写成
SETNX后再单独设置 TTLSETNX和EXPIRE如果分成两条命令,中间一旦程序宕机,可能出现“锁已经加上,但永不过期”的死锁。- 应使用一条原子命令完成“加锁 + 设置过期时间”,例如:
SET lock:key <uniqueToken> NX PX 30000。
-
把获取锁失败后的等待理解成 CAS 自旋
- Redis 分布式锁的竞争并不是 Java CAS。
- 获取失败后可以直接返回失败,也可以采用“重试 + sleep/退避/随机抖动”等方式再次尝试;不应无间隔地疯狂轮询 Redis。
-
只强调“TTL 不能短于业务执行时间”,但没有考虑业务超时和锁续期问题
- TTL 的主要作用是防止持锁客户端异常退出后形成永久死锁。
- 如果业务执行时间超过 TTL,锁会提前失效,其他客户端可能拿到同一把锁,导致两个客户端同时进入临界区。
- 对执行时间不可预测的任务,需要设置合理 TTL,并考虑自动续期(watchdog)等机制。
-
认为
finally里直接DEL key就足够安全finally解锁只能保证“尽量执行释放”,但不能保证“释放的是自己的锁”。- 每个持锁者都应把一个唯一 token(如 UUID)写入 value;解锁时只有 value 仍然等于自己的 token 才能删除。
- “比较 token + 删除 key”必须是原子操作,通常使用 Lua 脚本完成;不能先
GET再DEL,否则两条命令之间可能发生竞态。
-
没有考虑 Redis 主从切换带来的锁安全问题
- 在主从架构中,如果主节点刚写入锁但还未来得及复制就宕机,故障转移后新主节点可能没有这把锁,其他客户端就可能再次获得锁。
- 因此对强一致性要求很高的场景,需要评估 Redis 分布式锁在故障切换下的语义,或使用更适合强一致协调的方案。
核心记忆
加锁:
SET lock:key <唯一token> NX PX <TTL>,加锁和设置过期时间必须原子完成。
解锁:只能删自己的锁;用唯一 token 标识所有者,并通过 Lua 原子执行“比较 token + 删除”。
TTL 防止死锁,但 TTL 太短会导致“业务还没做完,锁先没了”;执行时间不可预测时要考虑续期。
获取失败不是 CAS;重试时应避免无间隔自旋打爆 Redis。
Spring:Bean 生命周期主要阶段
我的理解偏差
-
把
BeanDefinition写成BeanDefination,并把它理解成“根据 BeanDefinition 初始化 Bean”- 更准确地说,Spring 先根据
BeanDefinition创建 Bean 实例,也就是“实例化”;此时对象本身刚被创建出来,依赖还没有全部注入。
- 更准确地说,Spring 先根据
-
把
PostProcessBean当成生命周期组件名称- 正确名称是
BeanPostProcessor。 - 初始化前调用
postProcessBeforeInitialization(),初始化后调用postProcessAfterInitialization()。
- 正确名称是
-
只写了
@init,没有区分真正的初始化回调- 常见初始化回调包括
@PostConstruct、InitializingBean.afterPropertiesSet()和自定义init-method。 - 它们发生在
BeanPostProcessor的“初始化前”与“初始化后”之间。
- 常见初始化回调包括
-
把“BeanPostProcessor 后初始化”直接等同于“完成 AOP 代理”
- AOP 代理通常确实是在某些
BeanPostProcessor的postProcessAfterInitialization()阶段创建的。 - 但不是所有 Bean 都一定会被 AOP 代理,因此不能把生命周期第 6 步写成“必然完成 AOP 代理”。
- AOP 代理通常确实是在某些
-
把销毁阶段简单写成
@destroy- 常见销毁回调包括
@PreDestroy、DisposableBean.destroy()和自定义destroy-method。 - 一般是在 Spring 容器关闭时触发;prototype Bean 的完整销毁生命周期通常不由 Spring 容器负责。
- 常见销毁回调包括
核心记忆
Spring Bean 生命周期主线:实例化 → 属性赋值/依赖注入 → Aware 回调 → BeanPostProcessor 初始化前 → 初始化回调 → BeanPostProcessor 初始化后 → Bean 可用 → 容器关闭时执行销毁回调。
BeanPostProcessor:前处理 → 初始化 → 后处理。
AOP 代理通常在
postProcessAfterInitialization()阶段产生,但不是每个 Bean 都一定产生代理。
Spring Bean 生命周期:销毁注解
我的理解偏差
- 误以为 Spring Bean 生命周期中存在
@Destroy注解。
正确理解
- Spring / Jakarta 常用的销毁回调注解是
@PreDestroy,不是@Destroy。 - Spring Boot 3 / Spring Framework 6 使用
jakarta.annotation.PreDestroy。 - Bean 的常见销毁方式包括:
@PreDestroyDisposableBean.destroy()- 自定义
destroy-method
核心记忆
初始化常见
@PostConstruct,销毁常见@PreDestroy。 没有标准的@Destroy。
Spring Bean 生命周期:复答补充
本次仍需补充的点
-
初始化方法列举不完整
- 除了
@PostConstruct和自定义init-method,还常见InitializingBean.afterPropertiesSet()。
- 除了
-
第 6 步不能直接写成“执行 AOP 增强”
- 更准确地说:执行
BeanPostProcessor#postProcessAfterInitialization();如果某个 Bean 需要 AOP 增强,相关自动代理创建器可能在这一阶段返回代理对象。 - AOP 代理不是每个 Bean 都必经的生命周期步骤。
- 更准确地说:执行
核心记忆
初始化阶段常见顺序:
@PostConstruct→InitializingBean.afterPropertiesSet()→ 自定义init-method。
postProcessAfterInitialization()是扩展点;AOP 只是这个扩展点的一种典型用途。
评论
正在加载评论…