一、== 运算符

== 是 Java 中的一个运算符,用于比较两个对象,但在比较两个对象的时候需要根据比较类型分情况进行讨论。

1.1 基本数据类型与基本数据类型

基本数据类型之间通过 == 进行比较的时候,是直接比较它们的大小,而与它们的具体类型无关。

short num1 = 20000;
int num2 = 20000;
System.out.println(num1 == num2); // Output: true

1.2 引用类型与引用类型

引用类型之间是比较它们的内存地址,因此两个同类型的对象通过 == 进行比较时的结果一般为 false。

public class Test {
    public static void main(String[] args) {
        Obj o1 = new Obj();
        Obj o2 = new Obj();
        System.out.println(o1 == o2); // Output: false
    }
}

class Obj {}

但也不是所有情况都会返回 false,基本数据类型的引用类型有个常量池的机制,使得它们就算是两个对象,依然会指向同一片内存空间,也就是说,它们依然是同一个对象。

Integer num1 = 100, num2 = 100;
System.out.println(num1 == num2); // Output: true

具体内容请见:八大基本类型及其包装类型

1.3 基本数据类型与引用类型

当基本数据类型与引用类型之间进行比较时,若引用类型是数字类的,那么就可以进行比较,其他类型的无法进行比较,会报错。

Integer num1 = 200;
int num2 = 200;
System.out.println(num1 == num2); // Output: true

int num = 200;
String str = "123";
System.out.println(str == num); // Error: 二元运算符 '==' 的操作数类型错误

二、equals 方法

equals 是 Java 引用类型的一个基本方法,在没有对其进行重写(override)的情况下,equals 方法默认是比较两个对象的内存地址,重写之后视具体情况而定。

String str1 = "1";
String str2 = "2";
System.out.println(str1.equals(str2)); // Output: false

2.1 equals 方法和 == 运算符的区别

  • == 是一个运算符,equals 是对象的方法(所以基本数据类型没有);
  • == 在比较基本数据类型时,是比较的值,比较对象的时候,是比较其内存地址;
  • equals 默认(没有被重写)是比较对象的内存地址,重写后是其他的内容(如 String 就是其对象所表示内容的值)

三、hashcode 方法

hashcode 是一个非常特殊的方法,它涉及到散列表的知识,哈希(hash)翻译过来就是散列的意思,它是一种算法,可以快速将任意对象转换为一个 16 进制的数值,而不同的对象之间,通过哈希算法得到的哈希值大概率是不会相同的,有极小概率会相同,此时就出现了一种被称为“哈希冲突”的情况,这里我们不对其做深究,具体内容请参见:哈希算法

总之,在没有出现“哈希冲突”的情况下,我们就可以通过不同对象的哈希值不相同这一特性来判断两个对象是否为同一对象。

String str1 = "1";
String str2 = "2";
System.out.println(str1.hashCode() == str2.hashCode()); // Output: false

当然,一般情况下我们是不会像上面这样用 hashcode 这一方法的,因为无法保证不会出现“哈希冲突”的情况。虽然不同对象的哈希值有可能相同,但是两个对象的哈希值的不同则说明它们一定不相同!因此,哈希值可以用于判断两个对象是否不等。

四、区别和联系

4.1 区别

== 一般用来判断数字是否相等,equals 一般用来判断两个对象是否相等,而 hashcode 一般用来判断两个对象是否不等。

== 只比较数字,判断速度最快,此外是 hashcode 方法,因为哈希值的计算十分快速,最后是 equals,默认情况下 equals 是比较两个对象的内存地址,这一过程虽然很快速,但更一般的情况是 equals 方法被重写了,比较的是两个非数字类型对象的属性值,因此它最慢。

String str1 = "1";
String str2 = "1";
System.out.println(str1.equals(str2)); // Output: true

上面的代码中,Java 内置引用类型 String 就重写了 equals 方法,它比较的是两个 String 对象的内容,而非内存地址。

4.2 联系

== 一般和 equals 和 hashcode 扯不上什么关系,从某种角度上来说,equals 和 hashcode 之间也没有必然联系。但是 Java 中存在一种数据结构,将 equals 和 hashcode 联系了起来,那就是 HashMap 及其子类。

HashMap 中键是不可以重复的,因此它的键就必须是不同的对象,那么这个时候就先用计算速度快的 hashcode 进行比较,若哈希值都不相等,那么这两个对象必然不相等,若是相等的,那么这个就有两种可能出现,一种情况是这两个对象是真的相等,另外一种情况就是出现了罕见的“哈希冲突”现象,那么这个时候就轮到 equals 来进行判断了!这样一来,就高效且快速地解决了键不可重复的问题。

从上面 HashMap 的比较中我们也可以看到,当我们自定义类型的时候,若要重写 equals 方法或者 hashcode 方法,请将它们两个同时进行重写,因为 Java 内置数据结构 HashMap 等判断键是否重复是需要同时调用它们两个。

综上,我们可以总结为下表:

4.3 经典问题

为什么重写了 hashcode 方法还要重写 equals 方法?如果只重写 equals 方法而不重写 hashcode 方法会有问题吗?在 HashMap 中怎样体现出来的?

在 HashMap 中,键是不可以重复的,也就是说,它们的键都是不相同的,因此就要判断不同对象是否为同一对象。为了解决这个问题,HashMap 中同时调用了对象的 equals 方法和 hashcode 方法来进行判断。判断的部分源代码是这样的:

if (e.hash == hash &&
    ((k = e.key) == key || (key != null && key.equals(k))))
    return e;

hashcode 方法和 equals 方法都可以用来比较两个对象,但是二者有一些区别:

因此先用计算速度快的 hashcode 进行比较,这可以解决大部分问题,但由于可能出现“哈希冲突”,于是还需要 equals 方法解决。

正因为在判断键是否重复这一问题需要同时调用 equals 方法和 hashcode 方法,故我们自己定义的类中重写它们时必须两个一起重写。若只重写 equals 方法,而不重写 hashcode 方法,那么创建两个内容一样但内存地址不同的对象并存储在 HashMap 中时,会被当成两个键,而不是一个键,进而引发其他错误。

08-15 23:06