== 与 equals:到底比较什么?

在写代码的时候,你有没有被这个问题坑过?

String a = new String("hello");
String b = new String("hello");
System.out.println(a == b);        // 输出什么?
System.out.println(a.equals(b));   // 输出什么?

很多人会说一个是 true,一个是 false。但如果我追问:"为什么 equals 不是 == ?"很多人就答不上来了。

这个问题是 Java 基础中的基础,但恰恰是最容易被忽略的细节。面试中经常会延伸问到 String 的 intern 方法、hashCode 的重要性等等。今天我们就来把这个知识点彻底讲透。

一、真实面试场景

候选人小王在面试字节跳动的时候,被问到这道题:

String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");

System.out.println(s1 == s2);      // 问:输出什么?
System.out.println(s1 == s3);      // 问:输出什么?
System.out.println(s1.equals(s3)); // 问:输出什么?

小王回答:"s1 == s2 是 true,s1 == s3 是 false,s1.equals(s3) 是 true。"

面试官点点头,继续追问:"那为什么 s1 == s2 是 true,但 s1 == s3 是 false?"

小王说:"因为 s3 是 new 出来的,在堆里创建了新对象..."

面试官又问:"那 equals 呢?为什么 s1.equals(s3) 是 true?String 的 equals 是怎么实现的?"

小王开始支支吾吾。

【面试官心理】 这个问题看起来是考 == 和 equals 的区别,实际上我想知道的是:候选人有没有理解 String 的不可变性、字符串常量池、equals 的重写逻辑,以及 hashCode 和 equals 的关系。任何一个问题延伸追问下去,都能暴露他到底是真的理解还是只是"知道结论"。

二、== 的本质:比较的是什么?

2.1 基本类型:比较值

对于基本类型,== 比较的是本身:

int a = 10;
int b = 10;
System.out.println(a == b);  // true,值相等

double d1 = 0.1;
double d2 = 0.1;
System.out.println(d1 == d2);  // true,值相等

char c1 = 'A';
char c2 = 65;  // 'A' 的 ASCII 码是 65
System.out.println(c1 == c2);  // true,字符比较的是 Unicode 值

2.2 引用类型:比较地址(引用)

对于引用类型,== 比较的是内存地址,也就是"是不是同一个对象":

Person p1 = new Person("张三");
Person p2 = new Person("张三");
Person p3 = p1;  // p3 和 p1 指向同一个对象

System.out.println(p1 == p2);  // false,不同对象,地址不同
System.out.println(p1 == p3);  // true,p1 和 p3 是同一个对象

2.3 ❌ 常见错误:认为 == 比较的是内容

String s1 = new String("hello");
String s2 = new String("hello");

System.out.println(s1 == s2);  // false!不是比较内容,是比较地址

很多人会误以为 s1 == s2 应该输出 true,因为内容相同。但实际上 == 比较的是地址,不是内容。两次 new String() 创建了两个不同的对象,地址不同,所以是 false

⚠️

记住一个原则:== 永远比较的是"是不是同一个东西",而不是"内容是否相等"。基本类型比较值,引用类型比较地址。

三、equals 的本质:可重写的比较方法

3.1 Object 类的 equals 默认实现

在 Object 类中,equals 方法的默认实现就是 ==

public class Object {
    public boolean equals(Object obj) {
        return (this == obj);  // 调用的就是 ==
    }
}

所以如果一个类没有重写 equals,那么 equals 和 == 效果一样——比较地址。

3.2 为什么 String 要重写 equals?

因为 String 的业务场景需要比较"内容"而不是"地址":

String s1 = new String("hello");
String s2 = new String("hello");

// 没有重写 equals 的话,下面的结果应该是 false
System.out.println(s1.equals(s2));  // true,因为 String 重写了 equals

3.3 String equals 的实现原理

public boolean equals(Object anObject) {
    // 1. 先比较地址,如果是同一个对象直接返回 true
    if (this == anObject) {
        return true;
    }
    
    // 2. 再比较内容
    if (anObject instanceof String) {
        String anotherString = (String) anObject;
        int n = value.length;
        // 比较字符数组的每一个字符
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

这个实现做了两件事:

  1. 快速判断:先比较地址,同一个对象直接返回 true
  2. 内容比较:再逐字符比较字符数组的内容

3.4 【直观类比】equals 的重写逻辑

想象你在图书馆找书:

  • == 比较:比较的是"是不是同一本书"(同一个物理位置)
  • equals 比较:比较的是"内容是否相同"(同一个ISBN的书,虽然不是同一本纸,但内容一样)

String 重写 equals,就是让它按照 ISBN 来比较,而不是按照"物理位置"。

四、equals 的契约:不得不注意的五个要点

如果你要重写 equals 方法,必须遵守 Object 规范中定义的五个契约:

4.1 自反性(Reflexive)

x.equals(x) == true  // 任何对象都等于自己

4.2 对称性(Symmetric)

x.equals(y) == y.equals(x)  // x和y的比较结果应该一致

错误示例:

// 错误的equals实现
class Person {
    String name;
    int age;

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Person) {
            Person p = (Person) obj;
            return this.name.equals(p.name) && this.age == p.age;
        }
        // 错误!当 obj 是字符串时会和 Person 比较不对称
        if (obj instanceof String) {
            return obj.equals(this.name);
        }
        return false;
    }
}

4.3 传递性(Transitive)

if (x.equals(y) && y.equals(z)) {
    x.equals(z) == true  // x也应该等于z
}

4.4 一致性(Consistent)

// 多次调用应该返回相同的结果(前提是对象没有被修改)
x.equals(y) == x.equals(y)  // 结果应该一致

4.5 非空性(Non-null)

x.equals(null) == false  // 任何对象都不等于 null

五、hashCode:与 equals 必须配对使用

5.1 为什么重写 equals 必须重写 hashCode?

这是 Object 规范中的一个重要约定:相等的对象必须有相等的 hashCode

class Person {
    String name;
    int age;

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Person) {
            Person p = (Person) obj;
            return this.name.equals(p.name) && this.age == p.age;
        }
        return false;
    }

    // 如果不重写 hashCode,这里就会违反约定
}

不重写 hashCode 会导致什么问题?看看 HashMap 的场景:

Map<Person, String> map = new HashMap<>();

Person p1 = new Person("张三", 25);
Person p2 = new Person("张三", 25);

map.put(p1, "工程师");

String result = map.get(p2);  // 结果可能是 null!

如果只重写 equals 不重写 hashCode,p1p2 的 hashCode 可能不同,导致 HashMap 把它们存在不同的桶里,get 的时候找不到。

⚠️

这是生产环境中一个非常容易踩的坑:两个对象 equals 为 true,但 hashCode 不相等。HashMap、HashSet 等基于哈希的集合会出问题,具体表现为"存进去但取不出来"。排查起来很隐蔽,因为两个对象的逻辑是"相等"的,但存储和查询的结果不一致。

5.2 hashCode 的实现原则

// 良好的 hashCode 实现示例
@Override
public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    result = 31 * result + age;
    return result;
}

hashCode 的实现原则:

  1. 一致性:同一个对象在不被修改的情况下,多次调用 hashCode 应该返回相同的值
  2. 效率:计算应该快
  3. 分布均匀:不同的对象尽量有不同的 hashCode,使哈希表分布均匀

六、常见面试题解析

6.1 String 的 == vs equals

String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");

System.out.println(s1 == s2);      // true
System.out.println(s1 == s3);      // false
System.out.println(s1.equals(s3)); // true

为什么?因为:

  1. s1s2 指向字符串常量池中的同一个对象(编译器优化,"hello" 被放在常量池)
  2. s3 是 new 出来的,在堆中创建了新对象,和常量池中的不是同一个
  3. String 重写了 equals,比较的是内容,所以 s1.equals(s3) 是 true

6.2 Integer 的自动装箱陷阱

Integer a = 127;
Integer b = 127;
System.out.println(a == b);       // true?

Integer c = 128;
Integer d = 128;
System.out.println(c == d);       // false?

这个结果取决于 Integer 的缓存机制:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

Integer 有缓存 -128127 的 Integer 对象。所以:

  • a == b:都在缓存范围内,返回同一个对象,true
  • c == d:超过缓存范围,每次都 new 新对象,不同地址,false
💡

Integer、Short、Byte、Long、Character 都有类似的缓存机制,只是缓存范围不同。Boolean 直接只有两个常量:Boolean.TRUEBoolean.FALSE。这是一个高频面试点。

6.3 包装类型与 == 的坑

Integer a = 1;
Integer b = 1;
System.out.println(a == b);  // true(缓存范围内)

Integer c = new Integer(1);
Integer d = new Integer(1);
System.out.println(c == d);  // false(new 的一定是新对象)

System.out.println(a == c);  // false(一个在缓存,一个new)

记住:new 出来的对象一定不在缓存里,== 比较的一定是不同的对象。

七、生产场景与避坑

7.1 ❌ 错误示范:使用 == 比较字符串内容

String input = getUserInput();
if (input == "admin") {  // 错误!可能永远不等于
    // 登录逻辑
}

正确做法:

String input = getUserInput();
if ("admin".equals(input)) {  // 正确:使用 equals
    // 登录逻辑
}

注意这里把字面量放前面,这样做有两个好处:

  1. 避免 input 为 null 时抛 NullPointerException
  2. 直接使用 String 的 equals 比较内容

7.2 ❌ 错误示范:重写 equals 不重写 hashCode

class Order {
    private String orderId;
    private int amount;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Order order = (Order) o;
        return amount == order.amount && Objects.equals(orderId, order.orderId);
    }

    // 忘记重写 hashCode,导致 HashSet/HashMap 行为异常
}

正确做法:

class Order {
    private String orderId;
    private int amount;

    @Override
    public boolean equals(Object o) {
        // ...
    }

    @Override
    public int hashCode() {
        return Objects.hash(orderId, amount);
    }
}

7.3 ✅ 正确示范:使用 Objects.equals 和 Objects.hash

import java.util.Objects;

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    return o instanceof Order &&
           Objects.equals(orderId, ((Order) o).orderId) &&
           amount == ((Order) o).amount;
}

@Override
public int hashCode() {
    return Objects.hash(orderId, amount);
}

使用 Objects 工具类可以让代码更简洁,同时正确处理 null 比较。

八、面试追问链

第一层:基础概念

面试官问:"== 和 equals 的区别是什么?"

标准回答:== 对于基本类型比较值,对于引用类型比较地址(是否是同一个对象)。equals 是 Object 的方法,默认实现和 == 一样,但可以被重写来比较内容。String 重写了 equals,所以可以比较内容。

第二层:实现原理

面试官追问:"String 的 equals 是怎么实现的?"

需要说明:先比较地址(快速判断),再逐字符比较字符数组。涉及到 instanceof 判断、长度比较、字符数组遍历。

第三层:hashCode 关系

面试官追问:"重写 equals 为什么要重写 hashCode?"

需要说明:基于哈希的集合(HashMap、HashSet)在查询时先比较 hashCode 再比较 equals。如果两个对象 equals 为 true 但 hashCode 不同,会导致这些集合无法正常工作。

第四层:实际应用

面试官追问:"你在项目中怎么用 equals?有没有踩过坑?"

可以举实际例子:比如在 Service 层比较业务对象时使用 equals,在使用 HashMap 做缓存时注意重写 hashCode 等等。

【面试官心理】 这个问题的追问空间很大。我可以从 == vs equals 延伸到 String 特性、Integer 缓存、hashCode 契约等等。真正理解这些知识的候选人,能从最基础的 == 讲到哈希表的工作原理,中间没有任何卡顿。

【学习小结】

  • ==:基本类型比较值,引用类型比较地址
  • equals:默认等价于 ==,可以被重写来比较内容
  • String 重写了 equals,所以可以比较内容
  • 重写 equals 必须同时重写 hashCode(哈希表契约)
  • 使用 "constant".equals(variable) 可以避免空指针