(点击
上方公众号
,可快速关注)
来源:伯乐在线专栏作者 - shine_zejian
链接:http://android.jobbole.com/84152/
点击 → 了解如何加入专栏作者
接上文
5.为什么重写equals()的同时还得重写hashCode()
这个问题之前我也很好奇,不过最后还是在书上得到了比较明朗的解释,当然这个问题主要是针对映射相关的操作(Map接口)。学过数据结构的同学都知道Map接口的类会使用到键对象的哈希码,当我们调用put方法或者get方法对Map容器进行操作时,都是根据键对象的哈希码来计算存储位置的,因此如果我们对哈希码的获取没有相关保证,就可能会得不到预期的结果。在java中,我们可以使用hashCode()来获取对象的哈希码,其值就是对象的存储地址,这个方法在Object类中声明,因此所有的子类都含有该方法。那我们先来认识一下hashCode()这个方法吧。hashCode的意思就是散列码,也就是哈希码,是由对象导出的一个整型值,散列码是没有规律的,如果x与y是两个不同的对象,那么x.hashCode()与y.hashCode()基本是不会相同的,下面通过String类的hashCode()计算一组散列码:
package
com
.
zejian
.
test
;
public
class
HashCodeTest
{
public
static
void
main
(
String
[]
args
)
{
int
hash
=
0
;
String
s
=
"ok"
;
StringBuilder
sb
=
new
StringBuilder
(
s
);
System
.
out
.
println
(
s
.
hashCode
()
+
" "
+
sb
.
hashCode
());
String
t
=
new
String
(
"ok"
);
StringBuilder
tb
=
new
StringBuilder
(
s
);
System
.
out
.
println
(
t
.
hashCode
()
+
" "
+
tb
.
hashCode
());
}
}
运行结果:
3548 1829164700
3548 2018699554
我们可以看出,字符串s与t拥有相同的散列码,这是因为字符串的散列码是由内容导出的。而字符串缓冲sb与tb却有着不同的散列码,这是因为StringBuilder没有重写hashCode方法,它的散列码是由Object类默认的hashCode方法计算出来的对象存储地址,所以散列码自然也就不同了。那么我们该如何重写出一个较好的hashCode方法呢,其实并不难,我们只要合理地组织对象的散列码,就能够让不同的对象产生比较均匀的散列码。例如下面的例子:
package
com
.
zejian
.
test
;
public
class
Model
{
private
String
name
;
private
double
salary
;
private
int
sex
;
@Override
public
int
hashCode
()
{
return
name
.
hashCode
()
+
new
Double
(
salary
).
hashCode
()
+
new
Integer
(
sex
).
hashCode
();
}
}
上面的代码我们通过合理的利用各个属性对象的散列码进行组合,最终便能产生一个相对比较好的或者说更加均匀的散列码,当然上面仅仅是个参考例子而已,我们也可以通过其他方式去实现,只要能使散列码更加均匀(所谓的均匀就是每个对象产生的散列码最好都不冲突)就行了。不过这里有点要注意的就是java 7中对hashCode方法做了两个改进,首先java发布者希望我们使用更加安全的调用方式来返回散列码,也就是使用null安全的方法Objects.hashCode(注意不是Object而是java.util.Objects)方法,这个方法的优点是如果参数为null,就只返回0,否则返回对象参数调用的hashCode的结果。Objects.hashCode 源码如下:
public
static
int
hashCode
(
Object
o
)
{
return
o
!=
null
?
o
.
hashCode
()
:
0
;
}
因此我们修改后的代码如下:
package
com
.
zejian
.
test
;
import
java
.
util
.
Objects
;
public
class
Model
{
private
String
name
;
private
double
salary
;
private
int
sex
;
@Override
public
int
hashCode
()
{
return
Objects
.
hashCode
(
name
)
+
new
Double
(
salary
).
hashCode
()
+
new
Integer
(
sex
).
hashCode
();
}
}
java 7还提供了另外一个方法java.util.Objects.hash(Object… objects),当我们需要组合多个散列值时可以调用该方法。进一步简化上述的代码:
package
com
.
zejian
.
test
;
import
java
.
util
.
Objects
;
public
class
Model
{
private
String
name
;
private
double
salary
;
private
int
sex
;
// @Override
// public int hashCode() {
// return Objects.hashCode(name)+new Double(salary).hashCode()
// + new Integer(sex).hashCode();
// }
@Override
public
int
hashCode
()
{
return
Objects
.
hash
(
name
,
salary
,
sex
);
}
}
好了,到此hashCode()该介绍的我们都说了,还有一点要说的如果我们提供的是一个数值类型的变量的话,那么我们可以调用Arrays.hashCode()来计算它的散列码,这个散列码是由数组元素的散列码组成的。接下来我们回归到我们之前的问题,重写equals方法时也必须重写hashCode方法。在Java API文档中关于hashCode方法有以下几点规定(原文来自java深入解析一书)。
-
在java应用程序执行期间,如果在equals方法比较中所用的信息没有被修改,那么在同一个对象上多次调用hashCode方法时必须一致地返回相同的整数。如果多次执行同一个应用时,不要求该整数必须相同。
-
如果两个对象通过调用equals方法是相等的,那么这两个对象调用hashCode方法必须返回相同的整数。
-
如果两个对象通过调用equals方法是不相等的,不要求这两个对象调用hashCode方法必须返回不同的整数。但是程序员应该意识到对不同的对象产生不同的hash值可以提供哈希表的性能。
通过前面的分析,我们知道在Object类中,hashCode方法是通过Object对象的地址计算出来的,因为Object对象只与自身相等,所以同一个对象的地址总是相等的,计算取得的哈希码也必然相等,对于不同的对象,由于地址不同,所获取的哈希码自然也不会相等。因此到这里我们就明白了,如果一个类重写了equals方法,但没有重写hashCode方法,将会直接违法了第2条规定,这样的话,如果我们通过映射表(Map接口)操作相关对象时,就无法达到我们预期想要的效果。如果大家不相信, 可以看看下面的例子(来自java深入解析一书)
package
com
.
zejian
.
test
;
import
java
.
util
.
HashMap
;
import
java
.
util
.
Map
;
public
class
MapTest
{
public
static
void
main
(
String
[]
args
)
{
Map
String
,
Value
>
map1
=
new
HashMap
String
,
Value
>
();
String
s1
=
new
String
(
"key"
);
String
s2
=
new
String
(
"key"
);
Value
value
=
new
Value
(
2
);
map1
.
put
(
s1
,
value
);
System
.
out
.
println
(
"s1.equals(s2):"
+
s1
.
equals
(
s2
));
System
.
out
.
println
(
"map1.get(s1):"
+
map1
.
get
(
s1
));
System
.
out
.
println
(
"map1.get(s2):"
+
map1
.
get
(
s2
));
Map
Key
,
Value
>
map2
=
new
HashMap
Key
,
Value
>
();
Key
k1
=
new
Key
(
"A"
);
Key
k2
=
new
Key
(
"A"
);
map2
.
put
(
k1
,
value
);
System
.
out
.
println
(
"k1.equals(k2):"
+
s1
.
equals
(
s2
));
System
.
out
.
println
(
"map2.get(k1):"
+
map2
.
get
(
k1
));
System
.
out
.
println
(
"map2.get(k2):"
+
map2
.
get
(
k2
));
}
/**
* 键
* @author zejian
*
*/
static
class
Key
{
private
String
k
;
public
Key
(
String
key
){
this
.
k
=
key
;
}
@Override
public
boolean
equals
(
Object
obj
)
{
if
(
obj
instanceof
Key
){
Key
key
=
(
Key
)
obj
;
return
k
.
equals
(
key
.
k
);
}
return
false
;
}
}
/**
* 值
* @author zejian
*
*/
static
class
Value
{
private
int
v
;
public
Value
(
int
v
){
this
.
v
=
v
;
}
@Override
public
String
toString
()
{
return
"类Value的值-->"
+
v
;
}
}
}
代码比较简单,我们就不过多解释了(注意Key类并没有重写hashCode方法),直接运行看结果
s1
.
equals
(
s2
)
:
true
map1
.
get
(
s1
)
:
类
Value
的值--
>
2
map1
.
get
(
s2
)
:
类
Value
的值--
>
2
k1
.
equals
(
k2
)
:
true
map2
.
get
(
k1
)
:
类
Value
的值--
>
2
map2
.
get
(
k2
)
:
null
对于s1和s2的结果,我们并不惊讶,因为相同的内容的s1和s2获取相同内的value这个很正常,因为String类重写了equals方法和hashCode方法,使其比较的是内容和获取的是内容的哈希码。但是对于k1和k2的结果就不太尽人意了,k1获取到的值是2,k2获取到的是null,这是为什么呢?想必大家已经发现了,Key只重写了equals方法并没有重写hashCode方法,这样的话,equals比较的确实是内容,而hashCode方法呢?没重写,那就肯定调用超类Object的hashCode方法,这样返回的不就是地址了吗?k1与k2属于两个不同的对象,返回的地址肯定不一样,所以现在我们知道调用map2.get(k2)为什么返回null了吧?那么该如何修改呢?很简单,我们要做也重写一下hashCode方法即可(如果参与equals方法比较的成员变量是引用类型的,则可以递归调用hashCode方法来实现):
@Override
public
int
hashCode
()
{
return
k
.
hashCode
();
}
再次运行:
s1
.
equals
(
s2
)
:
true
map1
.
get
(
s1
)
:
类
Value
的值--
>
2
map1
.
get
(
s2
)
:
类
Value
的值--
>
2
k1
.
equals
(
k2
)
:
true
map2
.
get
(
k1
)
:
类
Value
的值--
>
2
map2
.
get
(
k2
)
:
类
Value
的值--
>
2
6.重写equals()中getClass与instanceof的区别
虽然前面我们都在使用instanceof(当然前面我们是根据需求(批次相同即相等)而使用instanceof的),但是在重写equals() 方法时,一般都是推荐使用 getClass 来进行类型判断(除非所有的子类有统一的语义才使用instanceof),不是使用 instanceof。我们都知道 instanceof 的作用是判断其左边对象是否为其右边类的实例,返回 boolean 类型的数据。可以用来判断继承中的子类的实例是否为父类的实现。下来我们来看一个例子:父类Person
public
class
Person
{
protected
String
name
;
public
String
getName
()
{
return
name
;
}
public
void
setName
(
String
name
)
{
this
.
name
=
name
;
}
public
Person
(
String
name
){
this
.
name
=
name
;
}
public
boolean
equals
(