小编道行也没那么深,就用最通俗易懂的方式,来解释hashmap实现原理。本文基于JDK8分析HashMap(),我们从源码出发将主要分析讨论如下的几个知识点:
- HashMap的特点是什么?以及它的使用场景
- HashMap的数据结构?
- HashMap的工作原理是什么?
- equals和hashCode都有什么作用?
- 重写equals()为什么一定要重写hashCode()?
- HashMap里面的table数组为什么是2的N次方?
1、感知HashMap
我们首先进行如下操作:
HashMap<String, Integer> map = new HashMap<String, Integer>();
map.put("语文", 1);
map.put("数学", 2);
map.put("英语", 3);
map.put("历史", 4);
map.put("政治", 5);
map.put("地理", 6);
map.put("生物", 7);
map.put("化学", 8);
for(Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
接着利用debug模式,从数据结构上认知HashMap,循序渐进
以下是JDK8中HashMap的数据结构源码:
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
表,在首次使用时初始化,并根据需要调整大小。当分配时,长度总是2的幂。
*/
transient Node<K,V>[] table;
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
基本的哈希bin节点,用于大多数条目(内部类)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
2、HashMap的两个重要参数
/**
* The default initial capacity - MUST be a power of two.table的默认初始容量
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16/**
* The load factor used when none specified in constructor.(负载因子)
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
capacity就是初始化table时的数组容量,load factor指table的填充比例;当我们对迭代性能要求比较高时,我们首先不能把capacity设置的太大;同时load factor不要超过0.75,否则会明显增加冲突几率,降低HashMap性能
负载因子 * 容量 > 元素数量(put进去的元素个数)时,就需要调整容量(table的长度)为原来的2倍
3、HashMap的put(Key k,Value v)的原理
在分析源码之前,我们先看看大体思路:
1)当在第一次put时,先对table初始化,通过hash计算得到存放位置table[i],存放。
2)当再次put时,同样经过hash计算得到位置,则采用链表法解决冲突,存放在相同位置的next区域
3)在JDK8中设置了链表的默认阈值为8,如果超过这个值,则进行树化
4)如果节点已经存在就替换old value(保证key的唯一性)
5)如果bucket满了(超过load factor*current capacity),就要resize,变为原来2倍
面试题:解释HashMap的原理,数据量增大时,
在数据量小的时候,HashMap是按照链表的模式存储的。当数据量变大之后,为了进行快速的查找,会将这个链表变成红黑树(均衡二叉树),用hash码作为数据的定位来进行保存。
具体实现代码如下:
public V put(K key, V value) {
// 对key的hashCode()做hash
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// tab为空则创建
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算index得出存放的位置,并对null做处理
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 节点存在
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 该链为树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 该链为链表,循环此单链表,插入或者更新链表
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//当链表长度超过默认阈值,进行树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 写入
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 超过load factor*current capacity,resize()
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
4、get函数的实现
大致思路如下:
- bucket里的第一个节点,直接命中;
- 如果有冲突,则通过key.equals(k)去查找对应的entry
若为树,则在树中通过key.equals(k)查找,O(logn);
若为链表,则在链表中通过key.equals(k)查找,O(n)。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 直接命中
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 未命中
if ((e = first.next) != null) {
// 在树中get
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 在链表中get
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
为了避免篇幅过长,关于table的长度为什么必须是2的N次方,hash函数的实现以及对开始问题的回答,在下篇博客进行分析,请转最新JDK8HashMap实现原理(二)
欢迎大家留言交流,小编q:1298364867