Java中的equals方法和hashCode方法是Object中的,因此每一个对象都是有这两个方法的,有时候咱们须要实现特定需求,可能要重写这两个方法,今天就来介绍一些这两个方法的做用。
java
equals()和hashCode()方法是用来在同一类中作比较用的,尤为是在容器里如set存放同一类对象时用来判断放入的对象是否重复。
算法
这里咱们首先要明白一个问题:
app
equals()相等的两个对象,hashcode()必定相等,equals()不相等的两个对象,却并不能证实他们的hashcode()不相等。换句话说,equals()方法不相等的两个对象,hashCode()有可能相等。(个人理解是因为哈希码在生成的时候产生冲突形成的)
ide
在这里hashCode就比如字典里每一个字的索引,equals()比如比较的是字典里同一个字下的不一样词语。就好像在字典里查“自”这个字下的两个词语“本身”、“自发”,若是用equals()判断查询的词语相等那么就是同一个词语,好比equals()比较的两个词语都是“本身”,那么此时hashCode()方法获得的值也确定相等;若是用equals()方法比较的是“本身”和“自发”这两个词语,那么获得结果是不想等,可是这两个词都属于“自”这个字下的词语因此在查索引时相同,即:hashCode()相同。若是用equals()比较的是“本身”和“他们”这两个词语的话那么获得的结果也是不一样的,此时hashCode() 获得也是不一样的。函数
反过来:hashcode()不等,必定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。在object类中,hashcode()方法是本地方法,返回的是对象的地址值,而object类中的equals()方法比较的也是两个对象的地址值,若是equals()相等,说明两个对象地址值也相等,固然hashcode() 也就相等了;性能
同时hash算法对于查找元素提供了很高的效率
测试
若是想查找一个集合中是否包含有某个对象,大概的程序代码怎样写呢?
ui
你一般是逐一取出每一个元素与要查找的对象进行比较,当发现某个元素与要查找的对象进行equals方法比较的结果相等时,则中止继续查找并返回确定的信息,不然,返回否认的信息,若是一个集合中有不少个元素,好比有一万个元素,而且没有包含要查找的对象时,则意味着你的程序须要从集合中取出一万个元素进行逐一比较才能获得结论。this
有人发明了一种哈希算法来提升从集合中查找元素的效率,这种方式将集合分红若干个存储区域,每一个对象能够计算出一个哈希码,能够将哈希码分组(使用不一样的hash函数来计算的),每组分别对应某个存储区域,根据一个对象的哈希吗就能够肯定该对象应该存储在哪一个区域HashSet就是采用哈希算法存取对象的集合,它内部采用对某个数字n进行取余(这种的hash函数是最简单的)的方式对哈希码进行分组和划分对象的存储区域;Object类中定义了一个hashCode()方法来返回每一个Java对象的哈希码,当从HashSet集合中查找某个对象时,Java系统首先调用对象的hashCode()方法得到该对象的哈希码表,而后根据哈希吗找到相应的存储区域,最后取得该存储区域内的每一个元素与该对象进行equals方法比较;这样就不用遍历集合中的全部元素就能够获得结论,可见,HashSet集合具备很好的对象检索性能,可是,HashSet集合存储对象的效率相对要低些,由于向HashSet集合中添加一个对象时,要先计算出对象的哈希码和根据这个哈希码肯定对象在集合中的存放位置为了保证一个类的实例对象能在HashSet正常存储,要求这个类的两个实例对象用equals()方法比较的结果相等时,他们的哈希码也必须相等;也就是说,若是obj1.equals(obj2)的结果为true,那么如下表达式的结果也要为true:
obj1.hashCode() == obj2.hashCode()
.net
换句话说:当咱们重写一个对象的equals方法,就必须重写他的hashCode方法,不过不重写他的hashCode方法的话,Object对象中的hashCode方法始终返回的是一个对象的hash地址,而这个地址是永远不相等的。因此这时候即便是重写了equals方法,也不会有特定的效果的,由于hashCode方法若是都不想等的话,就不会调用equals方法进行比较了,因此没有意义了。
若是一个类的hashCode()方法没有遵循上述要求,那么,当这个类的两个实例对象用equals()方法比较的结果相等时,他们原本应该没法被同时存储进set集合中,可是,若是将他们存储进HashSet集合中时,因为他们的hashCode()方法的返回值不一样(Object中的hashCode方法返回值是永远不一样的),第二个对象首先按照哈希码计算可能被放进与第一个对象不一样的区域中,这样,它就不可能与第一个对象进行equals方法比较了,也就可能被存储进HashSet集合中了,Object类中的hashCode()方法不能知足对象被存入到HashSet中的要求,由于它的返回值是经过对象的内存地址推算出来的,同一个对象在程序运行期间的任什么时候候返回的哈希值都是始终不变的,因此,只要是两个不一样的实例对象,即便他们的equals方法比较结果相等,他们默认的hashCode方法的返回值是不一样的。
下面来看一下一个具体的例子:
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
|
RectObject对象:
package
com.weijia.demo;
public
class
RectObject {
public
int
x;
public
int
y;
public
RectObject(
int
x,
int
y){
this
.x = x;
this
.y = y;
}
@Override
public
int
hashCode(){
final
int
prime =
31
;
int
result =
1
;
result = prime * result + x;
result = prime * result + y;
return
result;
}
@Override
public
boolean
equals(Object obj){
if
(
this
== obj)
return
true
;
if
(obj ==
null
)
return
false
;
if
(getClass() != obj.getClass())
return
false
;
final
RectObject other = (RectObject)obj;
if
(x != other.x){
return
false
;
}
if
(y != other.y){
return
false
;
}
return
true
;
}
}
|
咱们重写了父类Object中的hashCode和equals方法,看到hashCode和equals方法中,若是两个RectObject对象的x,y值相等的话他们的hashCode值是相等的,同时equals返回的是true;
下面是测试代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package
com.weijia.demo;
import
java.util.HashSet;
public
class
Demo {
public
static
void
main(String[] args){
HashSet<RectObject> set =
new
HashSet<RectObject>();
RectObject r1 =
new
RectObject(
3
,
3
);
RectObject r2 =
new
RectObject(
5
,
5
);
RectObject r3 =
new
RectObject(
3
,
3
);
set.add(r1);
set.add(r2);
set.add(r3);
set.add(r1);
System.out.println(
"size:"
+set.size());
}
}
|
咱们向HashSet中存入到了四个对象,打印set集合的大小,结果是多少呢?
运行结果:size:2
为何会是2呢?这个很简单了吧,由于咱们重写了RectObject类的hashCode方法,只要RectObject对象的x,y属性值相等那么他的hashCode值也是相等的,因此先比较hashCode的值,r1和r2对象的x,y属性值不等,因此他们的hashCode不相同的,因此r2对象能够放进去,可是r3对象的x,y属性值和r1对象的属性值相同的,因此hashCode是相等的,这时候在比较r1和r3的equals方法,由于他么两的x,y值是相等的,因此r1,r3对象是相等的,因此r3不能放进去了,一样最后再添加一个r1也是没有没有添加进去的,因此set集合中只有一个r1和r2这两个对象
下面咱们把RectObject对象中的hashCode方法注释,即不重写Object对象中的hashCode方法,在运行一下代码:
运行结果:size:3
这个结果也是很简单的,首先判断r1对象和r2对象的hashCode,由于Object中的hashCode方法返回的是对象本地内存地址的换算结果,不一样的实例对象的hashCode是不相同的,一样由于r3和r1的hashCode也是不相等的,可是r1==r1的,因此最后set集合中只有r1,r2,r3这三个对象,因此大小是3
下面咱们把RectObject对象中的equals方法中的内容注释,直接返回false,不注释hashCode方法,运行一下代码:
运行结果:size:3
这个结果就有点意外了,咱们来分析一下:
首先r1和r2的对象比较hashCode,不相等,因此r2放进set中,再来看一下r3,比较r1和r3的hashCode方法,是相等的,而后比较他们两的equals方法,由于equals方法始终返回false,因此r1和r3也是不相等的,r3和r2就不用说了,他们两的hashCode是不相等的,因此r3放进set中,再看r4,比较r1和r4发现hashCode是相等的,在比较equals方法,由于equals返回false,因此r1和r4不相等,同一r2和r4也是不相等的,r3和r4也是不相等的,因此r4能够放到set集合中,那么结果应该是size:4,那为何会是3呢?
这时候咱们就须要查看HashSet的源码了,下面是HashSet中的add方法的源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/**
* Adds the specified element to this set if it is not already present.
* More formally, adds the specified element <tt>e</tt> to this set if
* this set contains no element <tt>e2</tt> such that
* <tt>(e==null ? e2==null : e.equals(e2))</tt>.
* If this set already contains the element, the call leaves the set
* unchanged and returns <tt>false</tt>.
*
* @param e element to be added to this set
* @return <tt>true</tt> if this set did not already contain the specified
* element
*/
public
boolean
add(E e) {
return
map.put(e, PRESENT)==
null
;
}
|
这里咱们能够看到其实HashSet是基于HashMap实现的,咱们在点击HashMap的put方法,源码以下:
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
|
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public
V put(K key, V value) {
if
(key ==
null
)
return
putForNullKey(value);
int
hash = hash(key);
int
i = indexFor(hash, table.length);
for
(Entry<K,V> e = table[i]; e !=
null
; e = e.next) {
Object k;
if
(e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(
this
);
return
oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return
null
;
}
|
咱们主要来看一下if的判断条件,
首先是判断hashCode是否相等,不相等的话,直接跳过,相等的话,而后再来比较这两个对象是否相等或者这两个对象的equals方法,由于是进行的或操做,因此只要有一个成当即可,那这里咱们就能够解释了,其实上面的那个集合的大小是3,由于最后的一个r1没有放进去,觉得r1==r1返回true的,因此没有放进去了。因此集合的大小是3,若是咱们将hashCode方法设置成始终返回false的话,这个集合就是4了。
最后咱们在来看一下hashCode形成的内存泄露的问题:看一下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package
com.weijia.demo;
import
java.util.HashSet;
public
class
Demo {
public
static
void
main(String[] args){
HashSet<RectObject> set =
new
HashSet<RectObject>();
RectObject r1 =
new
RectObject(
3
,
3
);
RectObject r2 =
new
RectObject(
5
,
5
);
RectObject r3 =
new
RectObject(
3
,
3
);
set.add(r1);
set.add(r2);
set.add(r3);
r3.y =
7
;
System.out.println(
"删除前的大小size:"
+set.size());
set.remove(r3);
System.out.println(
"删除后的大小size:"
+set.size());
}
}
|
运行结果:
删除前的大小size:3
删除后的大小size:3
擦,发现一个问题了,并且是个大问题呀,咱们调用了remove删除r3对象,觉得删除了r3,但事实上并无删除,这就叫作内存泄露,就是不用的对象可是他还在内存中。因此咱们屡次这样操做以后,内存就爆了。看一下remove的源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/**
* Removes the specified element from this set if it is present.
* More formally, removes an element <tt>e</tt> such that
* <tt>(o==null ? e==null : o.equals(e))</tt>,
* if this set contains such an element. Returns <tt>true</tt> if
* this set contained the element (or equivalently, if this set
* changed as a result of the call). (This set will not contain the
* element once the call returns.)
*
* @param o object to be removed from this set, if present
* @return <tt>true</tt> if the set contained the specified element
*/
public
boolean
remove(Object o) {
return
map.remove(o)==PRESENT;
}
|
而后再看一下remove方法的源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/**
* Removes the mapping for the specified key from this map if present.
*
* @param key key whose mapping is to be removed from the map
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public
V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return
(e ==
null
?
null
: e.value);
}
|
在看一下removeEntryForKey方法源码:
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
|
/**
* Removes and returns the entry associated with the specified key
* in the HashMap. Returns null if the HashMap contains no mapping
* for this key.
*/
final
Entry<K,V> removeEntryForKey(Object key) {
int
hash = (key ==
null
) ?
0
: hash(key);
int
i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while
(e !=
null
) {
Entry<K,V> next = e.next;
Object k;
if
(e.hash == hash &&
((k = e.key) == key || (key !=
null
&& key.equals(k)))) {
modCount++;
size--;
if
(prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(
this
);
return
e;
}
prev = e;
e = next;
}
return
e;
}
|
咱们看到,在调用remove方法的时候,会先使用对象的hashCode值去找到这个对象,而后进行删除,这种问题就是由于咱们在修改了r3对象的y属性的值,又由于RectObject对象的hashCode方法中有y值参与运算,因此r3对象的hashCode就发生改变了,因此remove方法中并无找到r3了,因此删除失败。即r3的hashCode变了,可是他存储的位置没有更新,仍然在原来的位置上,因此当咱们用他的新的hashCode去找确定是找不到了。
其实上面的方法实现很简单的:以下图:
很简单的一个线性的hash表,使用的hash函数是mod,源码以下:
1
2
3
4
5
6
|
/**
* Returns index for hash code h.
*/
static
int
indexFor(
int
h,
int
length) {
return
h & (length-
1
);
}
|
这个其实就是mod运算,只是这种运算比%运算要高效。
1,2,3,4,5表示是mod的结果,每一个元素对应的是一个链表结构,因此说想删除一个Entry<K,V>的话,首先获得hashCode,从而获取到链表的头结点,而后再遍历这个链表,若是hashCode和equals相等就删除这个元素。
上面的这个内存泄露告诉我一个信息:若是咱们将对象的属性值参与了hashCode的运算中,在进行删除的时候,就不能对其属性值进行修改,不然会出现严重的问题。
其实咱们也能够看一下8种基本数据类型对应的对象类型和String类型的hashCode方法和equals方法。
其中8中基本类型的hashCode很简单就是直接返回他们的数值大小,String对象是经过一个复杂的计算方式,可是这种计算方式可以保证,若是这个字符串的值相等的话,他们的hashCode就是相等的。8种基本类型的equals方法就是直接比较数值,String类型的equals方法是比较字符串的值的。
来自:https://www.jb51.net/article/96138.htm