《effective java》(第2版)第九条“覆盖equals时总要覆盖hashCode”。
以下为整理对该两个方法的学习。通过实例说明,在使用hashMap、hashSet时必须覆写equals和hashcode原因。以及可能造成的错误。
各种情况:(1)equals和hashCode都没有覆写;(2)只覆写equals方法;(3)只覆写hashCode方法
Object中hashCode方法:由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)
Object中equals方法:即,对于任何非空引用值 x
和 y
,当且仅当x
和y
引用同一个对象时,此方法才返回true
(x == y
具有值true
)
public class Person {
private int age;
private String name;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return this.age;
}
public String getName() {
return this.name;
}
// 未实现equals和hashCode方法
}
(1)当equals和hashCode都没有覆写
public class HashTest extends TestCase {
Map<Person, String> map;
@Override
protected void setUp() throws Exception {
map = new HashMap<Person, String>();
}
/**
* Person没有覆写equals和hashcode方法
*/
public void testNoMethod() {
map.put(new Person(21, "a"), "a");
/* 由于没有覆写hashCode方法(使用Object的hashCode方法),两次new的Person虽然参数一致,但hashcode返回值不一样,所以无法拿到之前put的key */
assertFalse(map.get(new Person(21, "a")) != null);
}
}
(2)只覆写了equals方法
public boolean equals(Object obj) {
if (obj == this)
return true;
if (!(obj instanceof Person))
return false;
Person person = ((Person) obj);
return (this.age == person.getAge())
&& (this.name.equals(person.getName()));
}
public class HashTest extends TestCase {
Map<Person, String> map;
@Override
protected void setUp() throws Exception {
map = new HashMap<Person, String>();
}
/**
* Person只覆写了equals方法
*/
public void testOnlyEqualsMethod() {
map.put(new Person(21, "a"), "a");
/*
* 由于没有覆写hashCode方法(使用Object的hashCode方法),两次new的Person虽然参数一致,但hashcode返回值不一样,所以无法拿到之前put的key
*/
assertFalse(map.get(new Person(21, "a")) != null);
}
}
(3)只覆写hashCode方法
public int hashCode() {
final int hashMultiplier = 41;
int result = 7;
result = result * hashMultiplier + this.age;
result = result * hashMultiplier + this.name.hashCode();
return result;
}
/**
* Person只覆写了hashCode方法
*/
public void testOnlyHashCodeMethod() {
map.put(new Person(0, "a"), "a");
assertTrue(map.get(new Person(0, "a")) == null);
}
map.get(new Person(0, "a"))得到的内容会为null。当调用get方法时,会先调用该对象hashCode方法取哈希值,如果该哈希值在map中不存在返回null;如果存在会调用Person的equals方法,判断map中的hashCode相等对象与new Person(0, "a")的equals方法(本例子的情况),由于没有覆写equals方法,会调用Objece的equals方法,返回false,所以拿到null。
总结:
综上,当往map中put值时,首先调用该对象的hashCode方法计算哈希值,如果map中没有,就将该对象put到map中,如果该对象的哈希值在map中已经有,会调用该对象的equals方法,如果返回true,不会put到map中,返回false认为map中的对象和该对象不一样,会放到map中。
当调用map的get方法时,会先调用该对象hashCode方法取哈希值,如果该哈希值在map中不存在返回null;如果存在会调用equals方法,如果返回true拿到该对象,如果返回false返回null。
HashSet和HashMap类似。
对于哈希数据结构的集合,有两个概念,一个是“桶(bucket)”,一个是“装载因子”。
要想查找表中对象的位置,就要先计算它的哈希值,然后与桶的总数取余,所得到的结果就是保存这个元素的桶的索引。例如:如果某个对象的哈希值为76268,并且有128个桶,对象应该保存在第108号桶中(76268除以128余108)。
【实际情况要比该规则复杂,如下方代码(HashMap中的put方法),会将该对象K key的hashCode再次进行哈希:int hash = hash(key.hashCode());】
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
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;
}
上方代码中的table就是前面描述的“桶(bucket)概念”,int i = indexFor(hash, table.length);部分代码计算该对象应放到桶的索引位置,indexFor方法如下:
static int indexFor(int h, int length) {
return h & (length-1);
}
由于两个对象的哈希值可能一致,也就出现了两个对象放在一个桶中的情况,此时就会调用equals方法,如果返回true就认为该对象已存在,不进行存储了;如果返回false这种现象被称为散列冲突,这时,需要用新对象与桶中的所有对象进行比较,查看这个对象是否已经存在。
综上,对于HashSet当调用add时,先取hashcode,进行计算要保存到的桶索引,如果桶中没有其他元素,就直接放进去(不调用equals方法),如果桶中已经有元素,就会调用equals方法,判断新对象是否在桶中已经存在了,如果不存在,就放进去。
HashMap调用get方法时,先取hashcode,计算桶索引位置,然后到该桶中查找,查找时hashCode和equals两个方法都会调用(即使桶中只有一个元素),代码如下:
public V get(Object key) {
if (key == null)
return getForNullKey();
// 计算哈希值
int hash = hash(key.hashCode());
// 根据索引indexFor取得对应的桶
for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
当满足”if (e.hash == hash && ((k = e.key) == key || key.equals(k)))“条件时,才认为找到,返回结果。以下测试说明该规则,代码:
// 计算后的桶的索引值不同
public void test04() {
Set<Person> set = new HashSet<Person>();
set.add(new Person(0, "a"));
set.add(new Person(1, "b"));
// 11864和11906是事先计算好的两个对象的hashCode值
System.out.println(hash(11864) & (16 - 1)); // 3
System.out.println(hash(11906) & (16 - 1)); // 5
System.out.println(set.size());
}
/**
* HashMap中的方法
*/
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
在执行完set的add方法后,打断点查看set,如下图
存储在了桶的索引位置3和5。以下为非同一个对象,但计算桶索引位置相同的情况:/**
* 计算后的桶的索引值相同
*/
public void test05() {
Set<Person> set = new HashSet<Person>();
set.add(new Person(22, "w"));
set.add(new Person(24, "y"));
// 11864和11906是事先计算好的两个对象的hashCode值
System.out.println(hash(12788) & (16 - 1)); // 11
System.out.println(hash(12872) & (16 - 1)); // 11
System.out.println(set.size());
}
/**
* HashMap中的方法
*/
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
上图中,两个对象都存储在了索引为11的桶中。
关于”装载因子“,测试理解如下:
/**
* 装载因子测试
*/
public void testLoadFactor() {
Set<Person> set = new HashSet<Person>();
/* 装载因子:0.75;table桶:16 */
set.add(new Person(0, "a"));
set.add(new Person(1, "b"));
set.add(new Person(2, "c"));
set.add(new Person(3, "d"));
set.add(new Person(4, "e"));
set.add(new Person(5, "f"));
set.add(new Person(6, "g"));
set.add(new Person(7, "h"));
set.add(new Person(8, "i"));
set.add(new Person(9, "j"));
set.add(new Person(10, "k"));
set.add(new Person(11, "l"));
/* 装载因子:0.75;table桶:32 【16*0.75=12,当放第13个元素时,会用双倍的桶数自动进行再散列】 */
set.add(new Person(12, "m"));
}
set.add(new Person(0, "a"));执行前截图:
set.add(new Person(12, "m"));执行后截图:
测试现象:装载因子默认值:0.75;