HashMap作为Java编程中最为常用的工具类之一,本文将从JDK1.8源码的层面对HashMap进行分析。
全限定类名:java.util.HashMap
一:HashMap概述
HashMap是实现了KEY-VALUE的非线程安全的数据结构。允许使用 null 值和 null 键。
二:HahsMap的内部是以何种形式存储数据的.
根据每个segment包含元素的具体数量以及MIN_TREEIFY_CAPACITY参数的限定共同决定使用链表还是红黑树。(在1.7以及之前只存在链表结构。关于红黑树的数据结构这边不予以详细介绍)
三:JDK1.8的HashMap与1.7中有何区别
JDK7与JDK8中HashMap实现的最大区别就是对于冲突的处理方法。由JDK7中的链表变为了JDK8中的链表、红黑树并存。
四:哪些优秀的内容本文没有提及的
五:源码分析及实现
1.常量说明
1.1 DEFAULT_INITIAL_CAPACITY 默认初始化容量。容量必须为2的次方。原因在下面hash的时候会解释。默认的hashmap大小为16.
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
1.2 MAXIMUM_CAPACITY 最大的容量大小2^30。
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
1.3 DEFAULT_LOAD_FACTOR默认resize的因子。0.75,即实际数量超过总数*DEFAULT_LOAD_FACTOR的数量即会发生resize动作。
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
1.4 TREEIFY_THRESHOLD 树化阈值。当单个segment的容量超过阈值时,将链表转化为红黑树。
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
1.5 UNTREEIFY_THRESHOLD 链表化阈值。当删除操作后单个segment的容量低于阈值时,将红黑树转化为链表。
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
1.6 MIN_TREEIFY_CAPACITY 最小树化容量。当桶中的bin被树化时最小的hash表容量,低于该容量时不会树化。
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
2.关键操作分析
2.1 如何分配到各个segment当中。
一致性算法一般包含mod或者是hash。HashMap是以hash操作作为散列依据。但是又与传统的hash存在着少许的优化。其hash值是key的hashcode与其hashcode右移16位的异或结果。在put方法中,将取出的hash值与当前的hashmap容量-1进行与运算。得到的就是位桶的下标。那么为何需要使用key.hashCode() ^ h>>>16的方式来计算hash值呢。其实从微观的角度来看,这种方法与直接去key的哈希值返回在功能实现上没有差别。但是由于最终获取下表是对二进制数组最后几位的与操作。所以直接取hash值会丢失高位的数据,从而增大冲突引起的可能。由于hash值是32位的二进制数。将高位的16位于低位的16位进行异或操作,即可将高位的信息存储到低位。因此该函数也叫做扰乱函数。目的就是减少冲突出现的可能性。而官方给出的测试报告也验证了这一点。直接使用key的hash算法与扰乱函数的hash算法冲突概率相差10%左右。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
... n = table.length
i = (n - 1) & hash
...
}
2.2 如何获取下一个需要扩容的大小。即如何获取距离一个数字的最近的2的幂次方。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
由于map的容量需要维持在2的幂次方,所以需要计算出下一个扩容的大小。具体算法如图所示,int n = cap-1;的目的是为了防止当前的大小正好为2的幂次。
二进制中一个大于0的数字必定有一位为1。假设一个数字为00001******。在进行第一次n|= n>>>1;后结果变为000011*****;即让最高位以及次一位的数字变为1.同理
下一步变为00001111****;即让后两位再变为一。以此类推直到数据变为000011111111.最后执行+1操作即得到了距离给定数字最近的二次幂。
2.3 如何根据key获取value。
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) {
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;
}
核心思想即通过传入的key,调用其hash值对segment的位置进行判断后。根据定位的segment对相应的链表/红黑树进行搜索。判断两个key的.equals()方法为真时,即取得
2.5 如何根据提交新的值。
源码:public V put(K key, V value) {
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;
if ((tab = table) == null || (n = tab.length) == 0) //(1)
n = (tab = resize()).length; //(2)
if ((p = tab[i = (n - 1) & hash]) == null) //(3)
tab[i] = newNode(hash, key, value, null); //(4)
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) //(5)
e = p; //(6)
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //(7)
else {
for (int binCount = 0; ; ++binCount) { //(7)
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //(8)
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); //无用操作,为linkedhashmap服务
return oldValue;
}
}
++modCount; //(9)
if (++size > threshold) //(10)
resize();
afterNodeInsertion(evict); //无用操作,为linkedhashmap服务
return null;
}
先判断当前的table是否为空(1),如果是则对table进行resize操作(2)。否则根据传入的key的hash算法进入到相应的segment(3)。如果segment对应的Node节点为空(4),2.6 如何删除一个值
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
删除操作可以参照上面的插入操作。进行反向思路即可。