HashMap作为我们最常用的数据结构集合之一,有着它特点。我们可以看看其特点。
首先,我们看看HashMap的数据结构模型。HashMap是数组和链表的集合。具有相同hash码为一个集合,以数组的形式存储。而每个hash码相同的数据,以链表的形式存储。
在jdk1.8的环境下,我们查看HashMap主要的方法:构造函数、put()和get()
我们先看看构造函数【默认的构造函数】:
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
我们看看loadFactor是容量因子的意思。loadFactor 容量因子为0.75。实际容量因子=实际存储数/容量大小。如果超过0.75,则会扩充容量。
put()方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
我们跟进了put()方法,却看到传入了五个参数。第一个是hash(key),然后才是…
我们看看hash()方法的实现:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
我们看看,hash(key)返回的内容是经过处理的hash值,我们可以通过该运算,减少hash冲突。hash冲突,可以参考:点击参考。经过此运算后,hash冲突的次数就降低了不少。
我们跟进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;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
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;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
putVal()方法有点长。不过我们慢慢分析。
首先,要知道HashMap有个属性是transient Node<K,V>[] table;
该属性就是上述图中的table。
执行tab=table,将table的内容给了tab。然后如果table内容为null,我们就调用resize()相当于重新分配table的大小。
执行p=tab[i = (n-1) & hash],也就是取tab内容中的第(n-1)&hash个。如果当前没null,也就是说第(n-1)&hash个的链表表头为null。我们就为tab[i]新建结点。
如果不为null,也就意味着第(n-1)&hash个是有值的,我们就遍历这个链表。如果找到合适的位置,就将结点加到该链表中。
而get()方法也是一样的。
我们看看get()方法的实现:
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
主要还是getNode()的实现
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) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
其实主要的思想也是一样的,首先先找到与hash(key)相同的table元素所在的下标,然后我们去遍历该下标。
了解了HashMap的源码结构了,我们可以做出以下总结:
1、HashMap改变了内容后,不能保证顺序是一样的。
2、HashMap中键值对的存储于value无关,只和hash(key)有关。
3、HashMap没有实现线程同步。
4、HashMap允许key和value值为null