Java对象的相等判定问题与equals方法详解

  1. 1. Integer
  2. 2. Double
  3. 3. 自定义类

本文介绍了 Java 中不同类的相等判定规则以及不同类中 equals 方法的效果。

1. Integer

首先我们来看以下这段代码的运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.yyj;

public class Equivalence {
public static void main(String[] args) {
testIntEqual(127);
testIntEqual(128);
/*
* [127] Auto: true true
* [127] valueOf: true true
* [127] Integer: false true
* [127] Int: true
* [128] Auto: false true
* [128] valueOf: false true
* [128] Integer: false true
* [128] Int: true
*/
}

private static void testIntEqual(int value) {
// 第一种定义方式,利用了自动装箱特性,推荐该写法
Integer a1 = value;
Integer b1 = value;
System.out.printf("[%d] Auto: %b %b\n", value, a1 == b1, a1.equals(b1));

// 第二种定义方式
Integer a2 = Integer.valueOf(value);
Integer b2 = Integer.valueOf(value);
System.out.printf("[%d] valueOf: %b %b\n", value, a2 == b2, a2.equals(b2));

// 第三种定义方式,Java9之后已弃用,效率远不如valueOf
Integer a3 = new Integer(value);
Integer b3 = new Integer(value);
System.out.printf("[%d] Integer: %b %b\n", value, a3 == b3, a3.equals(b3));

// 第四种定义方式,不使用包装类,无equals方法
int a4 = value;
int b4 = value;
System.out.printf("[%d] Int: %b\n", value, a4 == b4);
}
}

对于参数值127,== 操作符只在使用 new 创建对象的方式上判定两个数为 false,这是因为操作符 ==/!= 比较的是对象的引用,虽然参与比较的两个引用包含的内容相同,但他们指向了内存中的不同对象。注意这种定义方式在 Java9 之后已经被废弃,因为其效率远不如使用 valueOf 方法。

对于参数值128,可以发现所有创建方式创建的两个相同值的 Integer 对象在 == 操作符的判定下均为 false,这是因为出于效率的原因,Integer 会通过享元模式来缓存范围在-128~127内的对象,因此多次调用 Integer.valueOf(127) 生成的其实是同一个对象,而在此范围之外的值则不会这样,即每次调用 Integer.valueOf(128) 返回的都是不同的对象。

因此在使用 Integer 的时候如果需要比较值是否相等应该只是用 equals 方法。

2. Double

我们再来看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.yyj;

public class Equivalence {
public static void main(String[] args) {
testDoubleEqual(0, 0);
testDoubleEqual(0, Double.MIN_VALUE);
testDoubleEqual(Double.MAX_VALUE, Double.MAX_VALUE - Double.MIN_VALUE * 1000000);
/*
* [0.000000e+00 | 0.000000e+00] double: true
* [0.000000e+00 | 0.000000e+00] Auto: false true
* [0.000000e+00 | 0.000000e+00] valueOf: false true
* [0.000000e+00 | 0.000000e+00] Double: false true
* [0.000000e+00 | 4.900000e-324] double: false
* [0.000000e+00 | 4.900000e-324] Auto: false false
* [0.000000e+00 | 4.900000e-324] valueOf: false false
* [0.000000e+00 | 4.900000e-324] Double: false false
* [1.797693e+308 | 1.797693e+308] double: true
* [1.797693e+308 | 1.797693e+308] Auto: false true
* [1.797693e+308 | 1.797693e+308] valueOf: false true
* [1.797693e+308 | 1.797693e+308] Double: false true
*/
}

private static void testDoubleEqual(double a, double b) {
System.out.printf("[%e | %e] double: %b\n", a, b, a == b);

Double a1 = a;
Double b1 = b;
System.out.printf("[%e | %e] Auto: %b %b\n", a1, b1, a1 == b1, a1.equals(b1));

Double a2 = Double.valueOf(a);
Double b2 = Double.valueOf(b);
System.out.printf("[%e | %e] valueOf: %b %b\n", a2, b2, a2 == b2, a2.equals(b2));

Double a3 = new Double(a);
Double b3 = new Double(b);
System.out.printf("[%e | %e] Double: %b %b\n", a3, b3, a3 == b3, a3.equals(b3));
}
}

理论上浮点数的比较应该是很严格的,即两个数值之间即使有极小的不同也应该不相等。

例如0和 Double.MIN_VALUE 相比较并不相等,但是 Double.MAX_VALUE 减去一百万倍的 Double.MIN_VALUE 却仍等于 Double.MIN_VALUE,这是因为当一个非常大的数值减去一个相对较小的数值时,非常大的数值并不会发生显著变化,这叫做舍入误差,误差的产生原因是因为机器不能存储足够的信息来表示一个大数值的微小变化。

3. 自定义类

现在你是不是以为在比较对象内容是否相等的情况下都一律用 equals 函数即可?但是并没有这么简单。看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.yyj;

public class Equivalence {
public static void main(String[] args) {
testClassEqual();
}

private static void testClassEqual() {
A a1 = new A(10);
A a2 = new A(10);
System.out.println(a1.equals(a2)); // false

B b1 = new B(10);
B b2 = new B(10);
System.out.println(b1.equals(b2)); // true
}
}

class A {
int val;

A(int val) {
this.val = val;
}
}

class B {
int val;

B(int val) {
this.val = val;
}

public boolean equals(Object o) {
B tempB = (B)o; // 将Object对象转型为B
return this.val == tempB.val;
}
}

我们创建了两个类 A 的对象,且值相等,但是 equals 方法返回的结果为 false,这是因为 equals 方法的默认行为是比较引用,如果想比较内容必须像类 B 那样重写 equals 方法。

因此实际上其实是大多数标准库会重写 equals 方法来比较对象的内容而不是他们的引用。