== 和 equals() 有什么区别?
简化版
== 比的是值本身:基本类型比数值,引用类型比的是地址(是不是同一个对象)。equals() 是个方法,默认也比地址,但类通常会重写它来比业务内容是否相等(比如两个 String 字符相同就算相等)。所以简单记:== 比地址,equals() 比内容(前提是被重写了)。
详细版
== 分两种情况:
- 用在基本类型(
int、double、char…)上:比较数值,1 == 1为 true。 - 用在引用类型上:比较引用地址——两个变量是不是指向堆里同一个对象。
equals() 是 Object 的方法:
public boolean equals(Object obj) {
return this == obj; // Object 默认实现就是 ==,比地址
}
所以如果一个类没重写 equals(),它的 equals 和 == 效果一样(都比地址)。但 String、Integer 等都重写了 equals() 去比内容:
String a = new String("hi");
String b = new String("hi");
System.out.println(a == b); // false,两个不同对象,地址不同
System.out.println(a.equals(b)); // true,String 重写了 equals 比字符内容
完整版教学
一、为什么引用类型 == 比的是地址
Java 变量对引用类型存的是对象在堆中的地址(引用),不是对象本身。所以 a == b 问的是「a 和 b 这两个引用是不是指向同一块内存」,而不是「它们的内容一样吗」。
new String("hi") 每次都在堆上新建一个对象,两次 new 得到两个不同地址,== 自然 false。而它们的字符内容相同,重写过的 equals() 才返回 true。这就是「== 看是不是同一个,equals 看内容一不一样」。
二、String 的 == 陷阱:字面量 vs new
String a = "hi"; // 字面量,进字符串常量池
String b = "hi"; // 复用池中同一个对象
System.out.println(a == b); // true!指向常量池同一对象
String c = new String("hi"); // new 强制在堆上建新对象
System.out.println(a == c); // false,地址不同
System.out.println(a.equals(c)); // true,内容相同
字面量 "hi" 会进字符串常量池、相同内容复用一份,所以 a == b 是 true;而 new String() 一定新建对象,所以 a == c 是 false。这是面试最爱考的坑——比字符串内容永远用 equals(),别用 ==。
三、包装类的 == 陷阱:Integer 缓存
Integer x = 127, y = 127;
System.out.println(x == y); // true
Integer m = 128, n = 128;
System.out.println(m == n); // false!
Integer 有个 -128~127 的缓存池,这个范围内的值 Integer.valueOf() 会复用缓存对象,所以 127 == 127 为 true;超出范围(128)就 new 新对象,== 变 false。所以包装类比值也必须用 equals()(详见「自动装箱拆箱」那道题)。
⚠️ 一条铁律:只要是引用类型(对象),比内容就用
equals(),别用==。==只在基本类型、或确实要判断「是不是同一个对象」时才用。
四、equals 和 hashCode 的连带关系
重写 equals() 时几乎总要一起重写 hashCode(),否则对象放进 HashMap/HashSet 会出问题——两个 equals 相等的对象若 hashCode 不同,会被分到不同的桶,导致「放进去却找不到」。这部分详见「equals() 和 hashCode() 的关系」那道题。
五、null、安全比较与数组边界
调用实例方法需要接收者非空,因此 a.equals(b) 在 a == null 时会抛 NPE。Objects.equals(a, b) 把这套分支统一封装:两个都为空返回 true,只有一个为空返回 false,两个都非空才调用 a.equals(b)。这比在业务代码里散落判空更稳妥。
Objects.equals(null, null); // true
Objects.equals(null, "x"); // false
Objects.equals(new int[]{1}, new int[]{1}); // false,数组未按元素重写 equals
Arrays.equals(new int[]{1}, new int[]{1}); // true,逐元素比较
| 数据类型 | 内容比较方式 | 说明 |
|---|---|---|
| 基本类型 | == | 直接比较数值;浮点数还要考虑精度 |
| 普通对象 | Objects.equals(a, b) | null 安全,实际语义由类型的 equals 决定 |
| 一维数组 | Arrays.equals(a, b) | 逐元素比较 |
| 嵌套对象数组 | Arrays.deepEquals(a, b) | 递归比较嵌套数组 |
BigDecimal | 视业务选 equals 或 compareTo | equals 还比较 scale,如 1.0 与 1.00 不等 |
这说明“对象比内容用 equals”只是起点,还必须确认具体类型如何定义内容相等。业务判等从来不是由操作符自动替你决定的。
六、常见误区与追问
- 误区:用
==比较两个 String 内容。 常量池可能让错误写法偶尔返回 true,正确方式是内容比较。 - 误区:用
==比较包装类数值。 缓存和拆箱会改变表象,应避免依赖对象身份。 - 误区:认为所有类的
equals()都比较内容。 未重写时,Object.equals()仍然比较同一引用。 - 追问:两个数组为何
equals()为 false? 数组继承了 Object 的身份判等,应使用Arrays.equals或deepEquals。 - 追问:
BigDecimal("1.0").equals(new BigDecimal("1.00"))为何是 false? 它同时比较数值和 scale;忽略刻度时可用compareTo() == 0。 - 追问:枚举为什么常用
==? 每个枚举常量在 JVM 中是唯一实例,身份比较明确且 null 安全。
易错点:先问“我要比较身份还是业务值”,再问“这个类型怎样定义业务值”,最后才选择 API。
浮点数还有一条独立边界:计算结果常有舍入误差,0.1 + 0.2 == 0.3 通常为 false。此时问题不是引用身份,也不是 equals 是否重写,而是二进制浮点无法精确表示部分十进制小数。
double actual = 0.1 + 0.2;
boolean close = Math.abs(actual - 0.3) < 1e-9; // 按业务误差比较
金额等必须精确表达十进制的场景应使用 BigDecimal,并明确是否把 scale 纳入相等语义;科学计算则通常选择领域允许的误差阈值。
七、加强记忆
== 是语言操作符:基本类型比较数值,引用类型比较是否指向同一对象;equals() 是可重写的业务判等协议,默认实现仍等同身份比较。日常对象比较优先用 null 安全的 Objects.equals,数组使用 Arrays 工具,特殊数值类型还要理解其契约。常量池和包装缓存只会影响身份,不能拿来推断内容相等。