一、前言 Java技术面试的时候我们总会被问到这类的问题:重写equals()方法为什么一定要重写hashCode()方法?两个不相等的对象可以有相同的散列码吗?… 曾经对这些问题我也感到很困惑。equals()和hasCode()方法是Object类中的两个基本方法。在实际的应用程序中这两个方法起到了很重要的作用,比如在集合中查找元素,我们经常会根据实际需要重写这两个方法。下面就对equals()方法和hashCode()方法做一个详细的分析说明,希望对于有同样疑惑的人有些许帮助。二、重写equals()方法
1、为什么要重写equals()方法
我们都知道比较两个对象引用是否相同用==运算符,且只有当两个引用都引用相同对象时,使用==运算符才会返回true。而比较相同类型的两个不同对象的内容是否相同,可以使用equals()方法。但是Object中的equals()方法只使用==运算符进行比较,其源码如下:
1
public
boolean
equals(Object obj) {
2
return
(
this
== obj);
3
}
如果我们使用Object中的equals()方法判断相同类型的两个不同对象是否相等,则只有当两个对象引用都引用同一对象时才被视为相等。那么就存在这样一个问题:如果我们要在集合中查找该对象, 在我们不重写equals()方法的情况下,除非我们仍然持有这个对象的引用,否则我们永远找不到相等对象。
代码清单-1
1
List<String> test =
new
ArrayList<String>();
2
test.add(
"aaa"
);
3
test.add(
"bbb"
);
4
System.out.println(test.contains(
"bbb"
));
分析:ArrayList遍历它所有的元素并执行"bbb".equals(element)来判断元素是否和参数对象"bbb"相等。最终是由String类中重写的equals()方法来判断两个字符串是否相等。 2、怎样实现正确的equals()方法
首先,我们需要遵守Java API文档中equals()方法的约定,如下:
自反性:对于任何非空引用值 x,x.equals(x) 都应返回 true。
对称性:对于任何非空引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。
传递性:对于任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。
一致性:对于任何非空引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。
对于任何非空引用值 x,x.equals(null) 都应返回 false。
其次,当我们重写equals()方法时, 不同类型的属性比较方式不同,如下:
属性是Object类型, 包括集合: 使用equals()方法。
属性是类型安全的枚举: 使用equals()方法或==运算符(在这种情况下,它们是相同的)。
属性是可能为空的Object类型: 使用==运算符和equals()方法。
属性是数组类型: 使用Arrays.equals()方法。
属性是除float和double之外的基本类型: 使用==运算符。
属性是float: 使用Float.floatToIntBits方法转化成int,然后使用==运算符。
属性是double: 使用Double.doubleToLongBits方法转化成long , 然后使用==运算符。
值得注意的是,如果属性是基本类型的包装器类型(Integer,Boolean等等), 那么equals方法的实现就会简单一些,因为只需要递归调用equals()方法。
在equals()方法中,通常先执行最重要属性的比较,即最有可能不同的属性先进行比较。可以使用短路运算符&&来最小化执行时间。
3、一个简单的Demo
代码清单-2
01
/**
02
* 根据上面的策略写的一个工具类
03
*
04
*/
05
public
final
class
EqualsUtil {
06
07
public
static
boolean
areEqual(
boolean
aThis,
boolean
aThat) {
08
return
aThis == aThat;
09
}
10
11
public
static
boolean
areEqual(
char
aThis,
char
aThat) {
12
return
aThis == aThat;
13
}
14
15
public
static
boolean
areEqual(
long
aThis,
long
aThat) {
16
//注意byte, short, 和 int 可以通过隐式转换被这个方法处理
17
return
aThis == aThat;
18
}
19
20
public
static
boolean
areEqual(
float
aThis,
float
aThat) {
21
return
Float.floatToIntBits(aThis) == Float.floatToIntBits(aThat);
22
}
23
24
public
static
boolean
areEqual(
double
aThis,
double
aThat) {
25
return
Double.doubleToLongBits(aThis) == Double.doubleToLongBits(aThat);
26
}
27
28
/**
29
* 可能为空的对象属性
30
* 包括类型安全的枚举和集合, 但是不包含数组
31
*/
32
public
static
boolean
areEqual(Object aThis, Object aThat) {
33
return
aThis ==
null
? aThat ==
null
: aThis.equals(aThat);
34
}
35
}
Car类使用EqualsUtil来实现其equals()方法.
代码清单-3
001
import
java.util.ArrayList;
002
import
java.util.Arrays;
003
import
java.util.Date;
004
import
java.util.List;
005
006
public
final
class
Car {
007
008
private
String fName;
009
private
int
fNumDoors;
010
private
List<String> fOptions;
011
private
double
fGasMileage;
012
private
String fColor;
013
private
Date[] fMaintenanceChecks;
014
015
public
Car(String aName,
int
aNumDoors, List<String> aOptions,
double
aGasMileage, String aColor, Date[] aMaintenanceChecks) {
016
fName = aName;
017
fNumDoors = aNumDoors;
018
fOptions =
new
ArrayList<String>(aOptions);
019
fGasMileage = aGasMileage;
020
fColor = aColor;
021
fMaintenanceChecks =
new
Date[aMaintenanceChecks.length];
022
for
(
int
idx =
0
; idx < aMaintenanceChecks.length; ++idx) {
023
fMaintenanceChecks[idx] =
new
Date(aMaintenanceChecks[idx].getTime());
024
}
025
}
026
027
@Override
028
public
boolean
equals(Object aThat) {
029
//检查自身
030
if
(
this
== aThat)
return
true
;
031
032
//这里使用instanceof 而不是getClass有两个原因
033
//1. 如果需要的话, 它可以匹配任何超类型,而不仅仅是一个类;
034
//2. 它避免了冗余的校验"that == null" , 因为它已经检查了null - "null instanceof [type]" 总是返回false
035
if
(!(aThat
instanceof
Car))
return
false
;
036
//上面一行的另一种写法 :
037
//if ( aThat == null || aThat.getClass() != this.getClass() ) return false;
038
039
//现在转换成本地对象是安全的(不会抛出ClassCastException)
040
Car that = (Car) aThat;
041
042
//逐个属性的比较
043
return
044
EqualsUtil.areEqual(
this
.fName, that.fName) &&
045
EqualsUtil.areEqual(
this
.fNumDoors, that.fNumDoors) &&
046
EqualsUtil.areEqual(
this
.fOptions, that.fOptions) &&
047
EqualsUtil.areEqual(
this
.fGasMileage, that.fGasMileage) &&
048
EqualsUtil.areEqual(
this
.fColor, that.fColor) &&
049
Arrays.equals(
this
.fMaintenanceChecks, that.fMaintenanceChecks);
050
}
051
052
/**
053
* 测试equals()方法.
054
*/
055
public
static
void
main(String... aArguments) {
056
List<String> options =
new
ArrayList<String>();
057
options.add(
"sunroof"
);
058
Date[] dates =
new
Date[
1
];
059
dates[
0
] =
new
Date();
060
061
//创建一堆Car对象,仅有one和two应该是相等的
062
Car one =
new
Car(
"Nissan"
,
2
, options,
46.3
,
"Green"
, dates);
063
064
//two和one相等
065
Car two =
new
Car(
"Nissan"
,
2
, options,
46.3
,
"Green"
, dates);
066
067
//three仅有fName不同
068
Car three =
new
Car(
"Pontiac"
,
2
, options,
46.3
,
"Green"
, dates);
069
070
//four 仅有fNumDoors不同
071
Car four =
new
Car(
"Nissan"
,
4
, options,
46.3
,
"Green"
, dates);
072
073
//five仅有fOptions不同
074
List<String> optionsTwo =
new
ArrayList<String>();
075
optionsTwo.add(
"air conditioning"
);
076
Car five =
new
Car(
"Nissan"
,
2
, optionsTwo,
46.3
,
"Green"
, dates);
077
078
//six仅有fGasMileage不同
079
Car six =
new
Car(
"Nissan"
,
2
, options,
22.1
,
"Green"
, dates);
080
081
//seven仅有fColor不同
082
Car seven =
new
Car(
"Nissan"
,
2
, options,
46.3
,
"Fuchsia"
, dates);
083
084
//eight仅有fMaintenanceChecks不同
085
Date[] datesTwo =
new
Date[
1
];
086
datesTwo[
0
] =
new
Date(
1000000
);
087
Car eight =
new
Car(
"Nissan"
,
2
, options,
46.3
,
"Green"
, datesTwo);
088
089
System.out.println(
"one = one: "
+ one.equals(one));
090
System.out.println(
"one = two: "
+ one.equals(two));
091
System.out.println(
"two = one: "
+ two.equals(one));
092
System.out.println(
"one = three: "
+ one.equals(three));
093
System.out.println(
"one = four: "
+ one.equals(four));
094
System.out.println(
"one = five: "
+ one.equals(five));
095
System.out.println(
"one = six: "
+ one.equals(six));
096
System.out.println(
"one = seven: "
+ one.equals(seven));
097
System.out.println(
"one = eight: "
+ one.equals(eight));
098
System.out.println(
"one = null: "
+ one.equals(
null
));
099
}
100
}
输出结果如下:
01
one = one:
true
02
one = two:
true
03
two = one:
true
04
one = three:
false
05
one = four:
false
06
one = five:
false
07
one = six:
false
08
one = seven:
false
09
one = eight:
false
10
one =
null
:
false
三、重写hashCode()方法 1、为什么要重写hashCode()方法
在每个重写了equals()方法的类中也必须要重写hashCode()方法,如果不这样做就会违反Java API中Object类的hashCode()方法的约定,从而导致该类无法很好的用于基于散列的数据结构(HashSet、HashMap、Hashtable、LinkedHashSet、LinkedHashMap等等)。
下面是约定内容:
在 Java 应用程序执行期间,如果没有修改对象的equals()方法的比较操作所用到的信息,那么无论什么时候在同一对象上多次调用 hashCode 方法时,必须一致地返回同一个整数。同一应用程序的多次执行过程中,每次返回的整数可以不一致。
如果两个对象根据 equals(Object) 方法进行比较是相等的,那么调用这两个对象中任意一个对象的hashCode() 方法都必须产生相同的整数结果。
如果两个对象根据 equals(java.lang.Object) 方法进行比较是不相等的,那么调用这两个对象中任意一个对象的hashCode() 方法则不一定要产生不同的整数结果。但是,程序员应该知道,为不相等的对象产生不同整数结果可能会提高哈希表的性能。
因没有重写hashCode()方法而违反的约定是第二条:相等的对象必须具有相同的散列码。
我们来看看Object类中的hashCode()方法: public native int hashCode()。它默认总是为每个不同的对象产生不同的整数结果。即使我们重写equals()方法让类的两个截然不同的实例是相等的,但是根据Object.hashCode()方法,它们是完全不同的两个对象,即如果对象的散列码不能反映它们相等,那么对象怎么相等也没用。
下面是一段测试代码:
代码清单-4
01
public
class
EqualsAndHashcode {
02
static
class
Person {
03
private
String name;
04
private
Integer age;
05
public
Person(String name, Integer age) {
06
this
.name = name;
07
this
.age = age;
08
}
09
@Override
10
public
boolean
equals(Object o) {
11
if
(
this
== o)
return
true
;
12
if
(!(o
instanceof
Person))
return
false
;
13
Person person = (Person) o;
14
if
(!name.equals(person.name))
return
false
;
15
return
true
;
16
}
17
}
18
public
static
void
main(String[] args) {
19
Map<Person, String> map =
new
HashMap<Person, String>();
20
Person person1 =
new
Person(
"aaa"
,
22
);
21
map.put(person1,
"aaa"
);
22
Person person2 =
new
Person(
"aaa"
,
11
);
23
System.out.println(person1.equals(person2));
24
System.out.println(map.get(person2));
25
}
26
}
输出结果如下:
1
true
2
null
2、具有相同散列码的对象一定相等吗?
如果散列码不同,元素就会被放入集合中不同的bucket(桶)中。在实际的哈希算法中,在同一个桶(Bucket)内有多个元素的情形非常普遍,因为它们的散列码相同。这时候哈希检索就是一个两步的过程:
1) 使用hashCode()找到正确的桶(bucket)。 2) 使用equals()在桶内找到正确的元素。 所以除非使用equals()方法比较是相等的,否则相同散列码的对象还是不相等。
因此为了定位一个对象,查找对象和集合内的对象二者都必须具有相同的散列码,并且equals()方法也返回true。所以重写equals()方法也必须要重写hashCode()方法才能保证对象可以用作基于散列的集合。
3、如何实现性能好的hashCode()方法
无论所有实例变量是否相等都为其返回相同值,这样的hashCode()仍然是合法的,也是适当的。
代码清单-5
1
Override
2
public
int
hashCode() {
3
return
1492
;
4
}
它虽然不违反hashCode()方法的约定,但是它非常低效,因为所有的对象都放在一个bucket内,还是要通过equals()方法费力的找到正确的对象。
一个好的hashCode()方法通常倾向于“为不相等的对象产生不相等的散列码”。这正是hashCode()方法的第三条约定的含义。理想情况下,hashCode()方法应该产生均匀分布的散列码,将不相等的对象均匀分布到所有可能的散列值上。如果散列码都集中在一块儿,那么基于散列的集合在某些bucket的负载会很重。
在《Effective Java》一书中,Joshua Bloch对于怎样写出好的hashCode()方法给出了很好的指导,如下:
1、把某个非零的常数值,比如说17,保存在一个名为result的int类型的变量中。
2、对于对象中每个关键域f(指equals方法中涉及的每个域),完成以下步骤:
a.为该域计算int类型的散列码c: i.如果该域是boolean类型,则计算(f?1:0)。 ii.如果该域是byte、char、short或者int类型,则计算(int)f。 iii.如果该域是long类型,则计算(int)(f ^ (f >>> 32))。 iv.如果该域是float类型,则计算Float.floatToIntBits(f)。 v.如果该域是double类型,则计算Double.doubleToLongBits(f),然后按照步骤2.a.iii,为得到的long类型值计算散列值。 vi.如果该域是一个对象引用,并且该类的equals方法通过递归地调用equals的方式来 比较这个域,则同样为这个域递归地调用hashCode。如果需要更复杂的比较,则 为这个域计算一个“范式(canonical representation)”,然后针对这个范式调用hashCode。如果这个域的值为null,则返回0 (或者其他某个常数,但通常是0)。 vii. 如果该域是一个数组,则要把每一个元素当做单独的域来处理。也就是说,递归地应用上述规则,对每个重要的元素计算一个散列码,然后根据步骤2.b中的做法把这些散列值组合起来。如果数组域中的每个元素都很重要,可以利用发行版本1.5中增加的其中一个Arrays.hashCode方法。
b.按照下面的公式,把步骤2.a中计算得到的散列码c合并到result中:result *31* result+c; 3、返回result。
4、写完了hashCode方法之后,问问自己“相等的实例是否都具有相等的散列码”。要编写单元测试来验证你的推断。如果相等的实例有着不相等的散列码,则要找出原因,并修正错误。
下面是遵循这些指导的一个代码示例
代码清单-6
01
public
class
CountedString {
02
03
private
static
List<String> created =
new
ArrayList<String>();
04
private
String s;
05
private
int
id =
0
;
06
public
CountedString(String str) {
07
this
.s = str;
08
created.add(str);
09
for
(String s2 : created) {
10
if
(s2.equals(s)) {
11
id++;
12
}
13
}
14
}
15
16
@Override
17
public
String toString() {
18
return
"String: "
+ s +
", id="
+ id +
" hashCode(): "
+ hashCode();
19
}
20
21
@Override
22
public
boolean
equals(Object o) {
23
return
o
instanceof
CountedString && s.equals(((CountedString) o).s) && id == ((CountedString) o).id;
24
}
25
26
@Override
27
public
int
hashCode() {
28
int
result =
17
;
29
result =
37
* result + s.hashCode();
30
result =
37
* result + id;
31
return
result;
32
}
33
34
public
static
void
main(String[] args) {
35
Map<CountedString, Integer> map =
new
HashMap<CountedString, Integer>();
36
CountedString[] cs =
new
CountedString[
5
];
37
for
(
int
i =
0
; i < cs.length; i++) {
38
cs[i] =
new
CountedString(
"hi"
);
39
map.put(cs[i], i);
40
}
41
System.out.println(map);
42
for
(CountedString cstring : cs) {
43
System.out.println(
"Looking up "
+ cstring);
44
System.out.println(map.get(cstring));
45
}
46
}
47
}
输出结果如下:
01
{String: hi, id=
1
hashCode():
146447
=
0
, String: hi, id=
2
hashCode():
146448
=
1
, String: hi, id=
3
hashCode():
146449
=
2
, String: hi, id=
4
hashCode():
146450
=
3
, String: hi, id=
5
hashCode():
146451
=
4
}
02
Looking up String: hi, id=
1
hashCode():
146447
03
0
04
Looking up String: hi, id=
2
hashCode():
146448
05
1
06
Looking up String: hi, id=
3
hashCode():
146449
07
2
08
Looking up String: hi, id=
4
hashCode():
146450
09
3
10
Looking up String: hi, id=
5
hashCode():
146451
11
4
4、一个导致hashCode()方法失败的情形
我们都知道,序列化可保存对象,在以后可以通过反序列化再次得到该对象,但是对于transient变量,我们无法对其进行序列化,如果在hashCode()方法中包含一个transient变量,可能会导致放入集合中的对象无法找到。参见下面这个示例:
代码清单-7
01
public
class
SaveMe
implements
Serializable {
02
03
transient
int
x;
04
int
y;
05
public
SaveMe(
int
x,
int
y) {
06
this
.x = x;
07
this
.y = y;
08
}
09
10
@Override
11
public
boolean
equals(Object o) {
12
if
(
this
== o)
return
true
;
13
if
(!(o
instanceof
SaveMe))
return
false
;
14
SaveMe saveMe = (SaveMe) o;
15
if
(x != saveMe.x)
return
false
;
16
if
(y != saveMe.y)
return
false
;
17
return
true
;
18
}
19
20
@Override
21
public
int
hashCode() {
22
return
x ^ y;
23
}
24
25
@Override
26
public
String toString() {
27
return
"SaveMe{"
+
"x="
+ x +
", y="
+ y +
'}'
;
28
}
29
30
public
static
void
main(String[] args)
throws
IOException, ClassNotFoundException {
31
SaveMe a =
new
SaveMe(
9
,
5
);
32
// 打印对象
33
System.out.println(a);
34
Map<SaveMe, Integer> map =
new
HashMap<SaveMe, Integer>();
35
map.put(a,
10
);
36
// 序列化a
37
ByteArrayOutputStream baos =
new
ByteArrayOutputStream();
38
ObjectOutputStream oos =
new
ObjectOutputStream(baos);
39
oos.writeObject(a);
40
oos.flush();
41
// 反序列化a
42
ObjectInputStream ois=
new
ObjectInputStream(
new
ByteArrayInputStream(baos.toByteArray()));
43
SaveMe b = (SaveMe)ois.readObject();
44
// 打印反序列化后的对象
45
System.out.println(b);
46
// 使用反序列化后的对象检索对象
47
System.out.println(map.get(b));
48
}
49
}
输出结果如下:
1
SaveMe{x=
9
, y=
5
}
2
SaveMe{x=
0
, y=
5
}
3
null
从上面的测试可以知道,对象的transient变量反序列化后具有一个默认值,而不是对象保存(或放入HashMap)时该变量所具有的值。
五、总结
当重写equals()方法时,必须要重写hashCode()方法,特别是当对象用于基于散列的集合中时。
六、参考资料
http://www.ibm.com/developerworks/library/j-jtp05273/
http://www.javapractices.com/topic/TopicAction.do?Id=17
《Effective Java》
《Thinking in Java》
于是,月醉了,夜醉了,我也醉了。