【Java八股|第1篇】== 和 equals 的区别

前言

从这一篇开始,我们进入 Java 面试八股系列。

这一系列不只适合面试前背题,也适合用来补 Java 基础。很多问题看起来简单,但是如果只会一句"== 比较地址,equals 比较内容",面试时很容易被继续追问。

比如面试官可能会问:

  • ==equals 有什么区别?
  • String 使用 == 比较为什么可能是 true
  • equals 默认比较的是什么?
  • 为什么重写 equals 时通常也要重写 hashCode
  • 基本数据类型能不能调用 equals

这一篇我们就把 ==equals 从基础到面试回答完整梳理一遍。

一、先给结论

可以先记住一句话:

== 比较的是值。对于基本数据类型,比较的是具体数值;对于引用数据类型,比较的是对象地址。

equals 是 Object 类中的方法,默认比较对象地址,但很多类会重写它,用来比较对象内容。

这句话里面有几个重点:

  1. == 是运算符
  2. equals 是方法
  3. 基本数据类型没有 equals
  4. 引用类型使用 == 比较地址
  5. equals 默认也比较地址
  6. StringInteger 等类重写了 equals

下面逐个来看。

二、== 比较基本数据类型

对于基本数据类型,== 比较的是具体的值。

比如:

java 复制代码
public class Test {
    public static void main(String[] args) {
        int a = 10;
        int b = 10;

        System.out.println(a == b);
    }
}

输出结果:

text 复制代码
true

原因很简单,ab 都是 int 类型,并且值都是 10。

再看一个例子:

java 复制代码
public class Test {
    public static void main(String[] args) {
        char c = 'A';
        int num = 65;

        System.out.println(c == num);
    }
}

输出结果:

text 复制代码
true

因为字符 'A' 对应的编码值就是 65,所以比较结果为 true

这里说明一点:

基本数据类型使用 == 时,比较的是变量中保存的具体值。

常见基本数据类型包括:

text 复制代码
byte
short
int
long
float
double
char
boolean

它们不是对象,所以不能直接调用 equals 方法。

三、== 比较引用数据类型

引用数据类型使用 == 时,比较的是两个引用变量保存的地址是否相同。

先看一个例子:

java 复制代码
public class Test {
    public static void main(String[] args) {
        Student s1 = new Student("张三");
        Student s2 = new Student("张三");

        System.out.println(s1 == s2);
    }
}

class Student {
    private String name;

    public Student(String name) {
        this.name = name;
    }
}

输出结果:

text 复制代码
false

虽然两个学生的姓名都是 张三,但是它们是通过两次 new 创建出来的两个对象。

java 复制代码
Student s1 = new Student("张三");
Student s2 = new Student("张三");

这两个对象在堆内存中的地址不同,所以:

java 复制代码
s1 == s2

结果是 false

再看另一个例子:

java 复制代码
public class Test {
    public static void main(String[] args) {
        Student s1 = new Student("张三");
        Student s2 = s1;

        System.out.println(s1 == s2);
    }
}

输出结果:

text 复制代码
true

因为:

java 复制代码
Student s2 = s1;

这行代码不是创建新对象,而是让 s2 指向 s1 指向的同一个对象。

所以 s1s2 保存的对象地址相同,比较结果就是 true

四、equals 默认比较什么?

equalsObject 类中的方法。

所有 Java 类都直接或间接继承自 Object,所以所有对象都可以调用 equals 方法。

Object 类中 equals 的默认逻辑可以简单理解为:

java 复制代码
public boolean equals(Object obj) {
    return this == obj;
}

也就是说,如果一个类没有重写 equals 方法,那么它默认还是比较对象地址。

比如:

java 复制代码
public class Test {
    public static void main(String[] args) {
        Student s1 = new Student("张三");
        Student s2 = new Student("张三");

        System.out.println(s1.equals(s2));
    }
}

class Student {
    private String name;

    public Student(String name) {
        this.name = name;
    }
}

输出结果:

text 复制代码
false

原因是 Student 类没有重写 equals,所以调用的是 Object 默认的 equals 方法,本质上还是比较地址。

五、String 中的 equals 为什么比较内容?

String 类重写了 equals 方法,所以它比较的是字符串内容。

比如:

java 复制代码
public class Test {
    public static void main(String[] args) {
        String s1 = new String("hello");
        String s2 = new String("hello");

        System.out.println(s1 == s2);
        System.out.println(s1.equals(s2));
    }
}

输出结果:

text 复制代码
false
true

解释一下:

java 复制代码
s1 == s2

比较的是两个 String 对象的地址。因为它们是两次 new 出来的对象,所以地址不同,结果是 false

java 复制代码
s1.equals(s2)

比较的是字符串内容。两个字符串内容都是 hello,所以结果是 true

这就是面试中最常见的一组对比。

六、String 使用 == 为什么有时是 true?

看下面这个例子:

java 复制代码
public class Test {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hello";

        System.out.println(s1 == s2);
    }
}

输出结果:

text 复制代码
true

为什么这里 ==true

因为字符串字面量会放入字符串常量池。

java 复制代码
String s1 = "hello";
String s2 = "hello";

第一次出现 "hello" 时,会在字符串常量池中创建这个字符串。

第二次再写 "hello" 时,会直接复用常量池中已有的字符串对象。

所以 s1s2 指向的是同一个对象,使用 == 比较结果就是 true

但是如果写成:

java 复制代码
String s1 = new String("hello");
String s2 = new String("hello");

System.out.println(s1 == s2);

输出就是:

text 复制代码
false

因为 new String("hello") 会在堆中创建新的字符串对象。

七、自己定义类时如何重写 equals?

如果我们希望两个 Student 对象只要姓名相同,就认为它们相等,可以重写 equals

java 复制代码
import java.util.Objects;

public class Student {
    private String name;
    private Integer age;

    public Student(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }

        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        Student student = (Student) o;

        return Objects.equals(name, student.name)
                && Objects.equals(age, student.age);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

测试代码:

java 复制代码
public class Test {
    public static void main(String[] args) {
        Student s1 = new Student("张三", 18);
        Student s2 = new Student("张三", 18);

        System.out.println(s1 == s2);
        System.out.println(s1.equals(s2));
    }
}

输出结果:

text 复制代码
false
true

说明:

java 复制代码
s1 == s2

依旧比较地址,所以是 false

java 复制代码
s1.equals(s2)

调用的是我们重写后的 equals 方法,比较的是姓名和年龄,所以是 true

八、为什么重写 equals 通常也要重写 hashCode?

这是面试中很容易追问的点。

简单来说:

如果两个对象通过 equals 判断相等,那么它们的 hashCode 也应该相等。

这和 HashMapHashSet 这类集合有关。

比如我们把两个内容相同的 Student 放入 HashSet

java 复制代码
Set<Student> set = new HashSet<>();

set.add(new Student("张三", 18));
set.add(new Student("张三", 18));

System.out.println(set.size());

如果只重写 equals,不重写 hashCode,可能会导致集合认为它们是两个不同对象。

因为 HashSet 判断元素是否重复时,不只是看 equals,还会用到 hashCode

所以实际开发中,只要重写 equals,通常就要一起重写 hashCode

现在很多 IDE 都可以自动生成这两个方法。

九、基本数据类型能不能调用 equals?

不能。

比如:

java 复制代码
int a = 10;
int b = 10;

System.out.println(a.equals(b));

这是错误写法。

因为 int 是基本数据类型,不是对象,没有方法。

如果是包装类,就可以调用 equals

java 复制代码
Integer a = 10;
Integer b = 10;

System.out.println(a.equals(b));

输出结果:

text 复制代码
true

这里的 Integer 是包装类,是对象,所以可以调用 equals

不过包装类还涉及自动装箱、缓存机制等问题,后面可以单独讲。

十、常见面试回答

如果面试官问:

==equals 有什么区别?

可以这样回答:

== 是运算符,equals 是方法。

如果比较的是基本数据类型,== 比较的是具体的值。

如果比较的是引用数据类型,== 比较的是两个对象的内存地址,也就是是否指向同一个对象。

equalsObject 类中的方法,默认实现和 == 一样,也是比较地址。但是很多类会重写 equals,比如 StringInteger,它们重写后通常比较的是对象内容。

所以在比较字符串内容时,应该使用 equals,而不是使用 ==

十一、常见问题总结

1. String 比较内容应该用什么?

应该用:

java 复制代码
s1.equals(s2)

如果担心空指针,可以写成:

java 复制代码
"hello".equals(s1)

这样即使 s1null,也不会报空指针异常。

2. equals 一定比较内容吗?

不一定。

如果类没有重写 equals,默认还是比较地址。

只有重写了 equals,才可能按照内容比较。

3. == 一定比较地址吗?

不一定。

如果是基本数据类型,== 比较的是值。

如果是引用数据类型,== 比较的是地址。

4. 为什么两个 new String 使用 == 是 false?

因为它们是两个不同对象,地址不同。

java 复制代码
new String("hello") == new String("hello")

比较的是两个对象地址,所以是 false

5. 重写 equals 必须重写 hashCode 吗?

语法上不是必须,但是强烈建议一起重写。

否则在 HashMapHashSet 这类集合中可能出现问题。

十二、总结

这一篇主要学习了 ==equals 的区别。

== 是运算符。对于基本数据类型,它比较的是具体值;对于引用数据类型,它比较的是对象地址。

equalsObject 类中的方法,默认也是比较地址。但是像 StringInteger 这些类重写了 equals,所以它们可以用来比较内容。

面试时不要只背一句"== 比较地址,equals 比较内容",这句话并不完整。更准确的说法应该区分基本数据类型、引用数据类型,以及类有没有重写 equals

下一篇我们继续学习 Java 中非常经典的问题:String 为什么是不可变的。