数据结构
介于常用的jdk版本中, 对hashMap的结构设计有所调整
- 1.7
- 采用了数组 & 链表的结构形式
- 1.8
- 采用数组 & 链表 + 红黑树的结构形式
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;
}
}
操作方式
决定元素存放位置的是哈希码 : obj.hashCode(), 这是一个整型的数据, hashMap会对此值进行取模计算 (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
- initialize
- 负载因子
- 此参数决定了处在第一层的数组什么时候进行扩容
- 负载因子
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
- put
- 当数据容量达到负载因子所设置的阈值, 会扩容&重新计算索引位置
- 当某个桶位的链表长度大于8, 会转变为红黑树
- 当链表长度大于8 & 数组长度大于64就把链表下的所有节点转化为红黑树
- 当数据容量达到负载因子所设置的阈值, 会扩容&重新计算索引位置
//暴露在外的公共方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//put本身的私有实现
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//如果map为空, 则进行resize(回归为初始容量)操作
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//如果不为空, 则封装成node进行存放
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//说明map上有元素
else {
Node<K,V> e; K k;
//如果key相等, 则替换
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果当前节点是tree node类型的, 则调用putTreeVal()去存
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;
}
- get
//暴露在外的公共方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods.
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
//私有的get实现
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//首先通过寻址找到该key对应的数据节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//check第一个元素, 如果匹配, 则返回(如果只有一个元素, 便为数组), 不关心是否为链表或树类型
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
//如果是tree node类型节点, 则调用getTreeNode(), 开始取树的流程
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//如果是链表, 获取next元素, 匹配为true则返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
//map为空直接return null
return null;
}
hash
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//优化过后的hash算法使得高低位都能参与运算, 且尽可能少的产生hash冲突, 提高效率
/**
* h = key.hashCode() 表示 h 是 key 对象的 hashCode 返回值
* h >>> 16 是 h 右移 16 位,因为 int 是 4 字节,32 位,
* 所以右移 16 位后变成:左边 16 个 0 + 右边原 h 的高 16 位
*/
- 异或
- 二进制位运算, 如果一样则返回0, 不一样返回1
- 寻址优化
- putVal()中寻址的部分
- tab[i = (n - 1) & hash]
- 1.8之前的版本 : tab[i = n % hash]
- 其中n为哈希表长度, 在代码中规定, n一定为2的k次幂, 所以优化后的代码与取模的结果相同
- 不直接取模的原因
- 与运算对比取模, 计算机效率更好
- putVal()中寻址的部分
对比
简述1.7 or 1.8之间的区别
- 1.7扩容时需要重新计算哈希值和索引位置,1.8并不重新计算哈希值,巧妙地采用和扩容后容量进行&操作来计算新的索引位置
- 1.7是采用表头插入法插入链表,1.8采用的是尾部插入法
- 在1.7中采用表头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题;在1.8中采用尾部插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了
- 其中1.8中, 链表长度到达阈值时升级为红黑树的考虑是因为单纯的链表形式, 会存在安全问题 --- 可以模拟请求, 制造大量hash code相等的元素, 这就可能使得链表过长导致cpu资源消耗