规定/规则:通用约定是:相等的对象应该具有相同的hashCode
在每个覆盖了equals方法的类中,也必须覆盖hashCode方法,
如果不这样做的话,就会违反Object.hashCode的通用约定。
从而导致该类无法结合所有基于散列的集合(比如HashSet\HashMap\HashTable)一起正常运作。
简单的例子:定义HashCode为ID%8,比如我们的ID为9,9除8的余数为1,那么我们就把该类存在1这个位置,如果ID是13,求得的余数是5,那么我们就把该类放在5这个位置。依此类推。
如果两个类有相同的HashCode,例如9除以8和17除以8的余数都是1,(9和17有相同的哈希值,但他们不是同一个对象)也就是说,我们先通过 HashCode来判断两个类是否存放某个桶里,但这个桶里可能有很多类,那么我们就需要再通过equals在这个桶里找到我们要的类。
比较两个元素是否相同时,首先调用hashCode方法,如果没有重写hashCode方法,任何对象的hashCode方法是不相等的。(那就用不了hashCode判重(内容是否重复)了)
若不同,直接加入;若相同,再去调用equals方法,若相同,不加入,否则,加入。
HashMap工作:
HashMap
是由数组和链表组成的存储数据的结构。
那么是如何确定一个数据存储在数组中的哪个位置呢?
就是通过hashCode
方法进行计算出存储在哪个位置,
产生冲突的话就会调用equals
方法进行比对,
如果不同,那么就将其加入链表尾部,如果相同就替换原数据。
计算位置当然不是上面简单的一个hashCode
方法就计算出来,中间还有一些其他的步骤,这里可以简单的认为是hashCode
确定了位置。
put方法执行流程:
- 检查哈希表是否是个空表,如果是空表就调用inflateTable方法进行初始化
- 判断key是否为null,如果为null,就调用putForNullKey方法, 将key为null的key-value存储在哈希表的第一个位置中
- 如果key不为null,则调用hash方法计算key的hash值
- 根据hash值和Entry数组的长度定位到Entry数组的指定槽位i
- 判断Entry数组指定槽位的值e是否为null, 如果e不为null, 则遍历e指向的单链表, 如果传入的key在单链表中已经存在了, 就进行替换操作, 否则就新建一个Entry并添加到单链表的表头位置
- 如果e为null, 就新建一个Entry并添加到指定槽位
public V put(K key, V value) {
// 如果哈希表没有初始化就进行初始化
if (table == EMPTY_TABLE) {
// 初始化哈希表
inflateTable(threshold);
}
// 当key为null时,调用putForNullKey方法,保存null于table的第一个位置中,这是HashMap允许为null的原因
if (key == null) {
return putForNullKey(value);
}
// 计算key的hash值
int hash = hash(key);
// 根据key的hash值和数组的长度定位到entry数组的指定槽位
int i = indexFor(hash, table.length);
// 获取存放位置上的entry,如果该entry不为空,则遍历该entry所在的链表
for (Entry<K, V> e = table[i]; e != null; e = e.next) {
Object k;
// 通过key的hashCode和equals方法判断,key是否存在, 如果存在则用新的value取代旧的value,并返回旧的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 修改次数增加1
modCount++;
// 如果找不到链表 或者 遍历完链表后,发现key不存在,则创建一个新的Entry,并添加到HashMap中
addEntry(hash, key, value, i);
return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果键值对的总数大于等于阀值,且当前要插入的key-value没有发生hash碰撞,则进行扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
// 扩容到原来容量的两倍
resize(2 * table.length);
// 扩容后重新计算hash值
hash = (null != key) ? hash(key) : 0;
// 扩容后重新确定Entry数组的槽位
bucketIndex = indexFor(hash, table.length);
}
// 创建一个Entry对象,并添加到Entry数组的指定位置中
createEntry(hash, key, value, bucketIndex);
}
static class Entry<K, V> implements Map.Entry<K, V> {
final K key;
V value;
Entry<K, V> next; // 下一个Entry对象的引用
int hash; // 元素的hash, 其实就是key的hash值
}
// 空的哈希表
static final Entry<?, ?>[] EMPTY_TABLE = {};
// 实际使用的哈希表
transient Entry<K, V>[] table = (Entry<K, V>[]) EMPTY_TABLE;
6、HashMap是如何通过key的hash值来定位到Entry数组的指定槽位的?
static int indexFor(int h, int length) {
// 对hash值和length-1进行与运算来计算索引
return h & (length - 1);
}
数组的下标是根据传入的元素hashCode方法的返回值再和特定的值异或决定的。
问:为什么重载hashCode方法?
答:一般的地方不需要重载hashCode,只有当类需要放在HashTable、HashMap、HashSet等等hash结构的集合时才会重载hashCode,那么为什么要重载hashCode呢?
如果你重写了equals,比如说是基于对象的内容实现的,而保留hashCode的实现不变,那么很可能某两个对象明明是“相等”,而hashCode却不一样。
这样,当你用其中的一个作为键保存到hashMap、hasoTable或hashSet中,再以“相等的”找另一个作为键值去查找他们的时候 ,则根本找不到。
问:重写了equals没有重写hashcode会发生什么?
假如只重写equals而不重写hashcode,那么Student类的hashcode方法就是Object默认的hashcode方法,由于默认的hashcode方法是根据对象的内存地址经哈希算法得来的一个值
那么很可能某两个对象明明是“相等”,而hashCode却不一样。
这样,当你用其中的一个作为键保存到hashMap、hasoTable或hashSet中,再以“相等的”找另一个作为键值去查找他们的时候,则根本找不到。导致HashSet、HashMap不能正常的运作
三、equals方法和hashcode的关系?
通过前面这个例子,大概可以知道,先通过hashcode来比较,如果hashcode相等,那么就用equals方法来比较两个对象是否相等,用个例子说明:上面说的hash表中的8个位置,就好比8个桶,每个桶里能装很多的对象,对象A通过hash函数算法得到将它放到1号桶中,当然肯定有别的对象也会放到1号桶中,如果对象B也通过算法分到了1号桶,那么它如何识别桶中其他对象是否和它一样呢,这时候就需要equals方法来进行筛选了。
1、如果两个对象equals相等,那么这两个对象的HashCode一定也相同
2、如果两个对象的HashCode相同,不代表两个对象就相同,只能说明这两个对象在散列存储结构中,存放于同一个位置
这两条你们就能够理解了。
四、为什么equals方法重写的话,建议也一起重写hashcode方法?
(如果对象的equals方法被重写,那么对象的HashCode方法也尽量重写)
举个例子,其实就明白了这个道理,
比如:有个A类重写了equals方法,但是没有重写hashCode方法,看输出结果,对象a1和对象a2使用equals方法相等,按照上面的hashcode的用法,那么他们两个的hashcode肯定相等,但是这里由于没重写hashcode方法,他们两个hashcode并不一样,所以,我们在重写了equals方法后,尽量也重写了hashcode方法,通过一定的算法,使他们在equals相等时,也会有相同的hashcode值。
总结:重写hashcode是为了当equals相等时,这两对象的hashcode可以相等。但算法怎么实现,咱也不知道