相等判断既是 Java 基础问题,也是 HashMap、HashSet 正确使用的前提。先判断比较的是值、对象身份,还是业务定义的相等关系。
== 与 equals 的区别
基本类型的 == 比较值;引用类型的 == 比较是否指向同一个对象。equals 是方法,Object 的默认实现也比较对象身份,但 String 等类重写它以比较内容。
String a = "java";String b = new String("java");System.out.println(a == b); // falseSystem.out.println(a.equals(b)); // true比较可能为 null 的对象可以使用 Objects.equals(a, b)。String 的内容相等不意味着引用相同。
重写 equals 为什么也要重写 hashCode?
必须满足:equals 相等的对象,hashCode 必须相同;hashCode 相同的对象,equals 不一定相等。 哈希表先通过 hash 找候选位置,再通过 equals 判断是否为同一个逻辑 key。
如果按用户 ID 重写 equals,却没有相应重写 hashCode,两个逻辑上相等的对象可能落到不同桶,导致 HashSet 出现业务上的重复,或 HashMap 查不到对应值。
用作 key 的字段应尽量保持不变。对象入表后再修改参与 hashCode/equals 的字段,会破坏定位和比较关系,即使对象本身仍在表中,也可能无法按预期查到。
String 为什么不可变?
String 不提供修改内容的操作。拼接、替换等操作得到新的字符串结果,而不是修改原字符串。
不可变带来的好处包括安全共享、作为 Map key 时稳定、可以缓存哈希值,以及适合常量池复用。底层使用数组并不代表外部可以修改数组;也不要仅用“数组被 final 修饰”解释不可变,final 本身不能禁止数组元素变化。
字符串常量池与 intern
字符串字面量和编译期可确定的字符串常量表达式可复用常量池中的字符串。new String(...) 创建的对象与池中的字符串通常不是同一个对象。
String a = "ab";String b = "a" + "b";String part = "a";String c = part + "b";System.out.println(a == b); // true,编译期常量折叠System.out.println(a == c); // false,运行期拼接System.out.println(a == c.intern()); // trueintern() 返回池中对应内容的规范引用。实际业务比较内容仍使用 equals,不依赖常量池碰巧复用引用。HotSpot JDK 7 起,字符串常量池位于堆中;这与元空间中的类元数据不是同一个概念。
String、StringBuilder、StringBuffer
| 类型 | 是否可变 | 并发特征 | 典型场景 |
|---|---|---|---|
| String | 不可变 | 内容可以安全共享 | 保存、传递字符串 |
| StringBuilder | 可变 | 不保证线程安全 | 单线程或局部变量中的循环拼接 |
| StringBuffer | 可变 | 主要操作有同步保护 | 确实需要共享可变字符串的场景 |
少量 + 拼接可交给编译器处理;循环里持续累积字符串时,通常使用 StringBuilder。不要认为 StringBuffer 的多个方法组合天然是一个原子操作。
包装类缓存与拆箱
自动装箱将 int 等基本类型变成 Integer 等包装对象,自动拆箱做相反转换。Integer 装箱至少复用 -128 到 127 的缓存对象,但不能把缓存范围当作业务相等判断规则。
Integer a = 127, b = 127;Integer c = 128, d = 128;System.out.println(a == b); // trueSystem.out.println(c.equals(d)); // true,比较数值缓存上限可能受实现或配置影响,所以不要依赖 c == d 的结果。包装类型为 null 时拆箱会抛 NullPointerException,例如 Integer count = null; int n = count;。
金额为什么常用 BigDecimal?
float/double 用二进制浮点表示,不能精确表示所有十进制小数。金额通常用 BigDecimal,并明确精度、舍入规则。
用字符串构造,如 new BigDecimal("0.1"),或使用 BigDecimal.valueOf;直接 new BigDecimal(0.1) 会保留传入 double 的精度误差。BigDecimal 的 equals 同时比较数值和 scale,1.0 与 1.00 不相等;只比较数值大小用 compareTo。
面试回答模板
引用类型的 == 比较对象身份,equals 按类型定义比较逻辑相等。重写 equals 必须保持 hashCode 约定,因为哈希集合要先定位再比较。String 不可变,常量池可以复用字符串,但业务上仍按内容使用 equals。包装类缓存只能解释部分引用相同的现象,不能代替数值比较,拆箱前还要处理 null。
自测
- 两个 hashCode 相同的对象一定相等吗?不一定,可能发生哈希冲突。
- HashMap key 入表后修改参与 hashCode 的字段会怎样?可能导致后续查找或删除失败。
new BigDecimal("1.0").equals(new BigDecimal("1.00"))返回什么?false;compareTo 返回 0。