HashMap的工作原理是什么
1.HashMap的数据结构(jdk1.8之前):(数组+链表)底层是一个数组,数组的每一项是一个链表,每次新建一个map其实就是新建了一个数组。
2.链表:每次新建一个HashMap时,都会初始化一个table数组。table数组的元素为Entry节点。其中Entry为HashMap的内部类,它包含了键key、值value、下一个节点next,以及hash值,这是非常重要的,正是由于Entry才构成了table数组的项为链表。
transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
}
来一个非常形象的图(有点模糊,看个大概意思)
3.HashMap的存储:HashMap的主要方法就是get方法,也是最复杂的。这里涉及到的具体哈西算法我也没有深入研究,只研究了大概。先上代码。
public V put(K key, V value) {
if (key == null)
return putForNullKey(value); //null总是放在数组的第一个链表中
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
// 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//如果key在链表中已存在,则替换为新value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 如果i索引处的Entry为null,表明此处还没有Entry。
modCount++;
// 将key、value添加到i索引处。
addEntry(hash, key, value, i);
return null;
当调用了hashmap的put方法时
--①判断key值是否为空
----如果key值是null的话,则将它的value放到数组的第一个位置
--②key值不是null,计算key.hashcode()的hash()值,这一步是为了元素在数组的各位置上能够均匀散列分布。
--③使用indexFor()方法,结果为此元素在table中的位置i
--④这个位置上的链表
----如果 i 索引处的 Entry 不为 null,key值相同替换value,key值不同将新Entry插在链表头部
下面是addEntry()
void addEntry(int hash, K key, V value, int bucketIndex) {
// 获取指定 bucketIndex 索引处的 Entry
Entry<K,V> e = table[bucketIndex];
// 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 如果 Map 中的 key-value 对的数量超过了极限
if (size++ >= threshold)
// 把 table 对象的长度扩充到原来的2倍。
resize(2 * table.length);
}
4.get方法:如果put理解,那么get就很容易了
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
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;
}
首先依然是判断key值是否为空,如果不为空的话,计算key.hashcode()的hash(),得到在数组中的位置,循环链表,取出对应的value值。
5.面试:面试的时候,除了HashMap的工作原理,还问过我几个问题,在这里整理一下。
工作原理:
- HashMap在Map.Entry静态内部类实现中存储key-value对。HashMap使用哈希算法,在put和get方法中,它使用hashCode()和equals()方法。当我们通过传递key-value对调用put方法的时候,HashMap使用Key hashCode()和哈希算法来找出存储key-value对的索引。Entry存储在LinkedList中,所以如果存在entry,它使用equals()方法来检查传递的key是否已经存在,如果存在,它会覆盖value,如果不存在,它会创建一个新的entry然后保存。当我们通过传递key调用get方法时,它再次使用hashCode()来找到数组中的索引,然后使用equals()方法找出正确的Entry,然后返回它的值。
- 其它关于HashMap比较重要的问题是容量、负荷系数和阀值调整。HashMap默认的初始容量是16,负荷系数是0.75。阀值是为负荷系数乘以容量,无论何时我们尝试添加一个entry,如果map的大小比阀值大的时候,HashMap会对map的内容进行重新哈希,且使用更大的容量。容量总是2的幂,所以如果你知道你需要存储大量的key-value对,比如缓存从数据库里面拉取的数据,使用正确的容量和负荷系数对HashMap进行初始化是个不错的做法。
hashCode()和equals()方法有何重要性:
- HashMap使用Key对象的hashCode()和equals()方法去决定key-value对的索引。当我们试着从HashMap中获取值的时候,这些方法也会被用到。如果这些方法没有被正确地实现,在这种情况下,两个不同Key也许会产生相同的hashCode()和equals()输出,HashMap将会认为它们是相同的,然后覆盖它们,而非把它们存储到不同的地方。同样的,所有不允许存储重复数据的集合类都使用hashCode()和equals()去查找重复,所以正确实现它们非常重要。equals()和hashCode()的实现应该遵循以下规则
- (1)如果o1.equals(o2),那么o1.hashCode() == o2.hashCode()总是为true的。
- (2)如果o1.hashCode() == o2.hashCode(),并不意味着o1.equals(o2)会为true。
为什么重写equals()的时候一定要重写hashcode():
- HashMap中,如果要比较key是否相等,要同时使用这两个函数!因为自定义的类的hashcode()方法继承于Object类,其hashcode码为默认的内存地 址,这样即便有相同含义的两个对象,比较也是不相等的,例如,生成了两个“羊”对象,正常理解这两个对象应该是相等的,但如果你不重写hashcode()方法的话,则比较是不相等的。
总结
以上均为互联网搜索下的产物,肯定有不对的地方,也有不清楚的地方,由于hashmap很乱,深入研究起来十分复杂,如果有不对的地方将及时修正和补充。