一 特性
- 使用hash方法进行区分key
- 可以存在一个为null的key
- 线程不安全
- HashMap在多线程情况下,rehash过程中有可能导致Entry产生环形链
-
二 属性
//默认容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量,2的30次方。
static final int MAXIMUM_CAPACITY = 1 << 30;
//加载因子,用于扩容使用。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当某个桶节点数量大于8时,会转换为红黑树。
static final int TREEIFY_THRESHOLD = 8;
//当某个桶节点数量小于6时,会转换为链表,前提是它当前是红黑树结构。
static final int UNTREEIFY_THRESHOLD = 6;
//当整个hashMap中元素数量大于64时,也会进行转为红黑树结构。 只有链表中元素到达伐值之后才会进行树化
static final int MIN_TREEIFY_CAPACITY = 64;
//存储元素的数组,transient关键字表示该属性不能被序列化
transient Node<K,V>[] table;
//将数据转换成set的另一种存储形式,这个变量主要用于迭代功能。
transient Set<Map.Entry<K,V>> entrySet;
//元素数量
transient int size;
//统计该map修改的次数
transient int modCount;
//临界值,也就是元素数量达到临界值时,会进行扩容。
int threshold;
//也是加载因子,只不过这个是变量。
final float loadFactor;
三 重要方法
tableSizeFor
- 返回大于输入参数且最近的2的整数次幂的数 这里8针对7进行了优化
- hash
- 将key的hash值右移动16位后再做异或运算,也是这里限制了容量必须是2的N次方
resize
HashMap(int initialCapacity, float loadFactor)
- 使用给定的容量和加载因子初始化
- HashMap(int initialCapacity)
- 使用给定的容量初始化
- HashMap()
- 初始化空的
HashMap(Map<? extends K, ? extends V> m)
-
五 添加元素
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) {
//tab 哈希数组,p 该哈希桶的首节点,n hashMap的长度,i 计算出的数组下标
Node<K,V>[] tab; Node<K,V> p; int n, i;
//获取长度并进行扩容,使用的是懒加载,table一开始是没有加载的,等put后才开始加载 调用resize进行加载
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/**如果计算出的该哈希桶的位置没有值,则把新插入的key-value放到此处,此处就算没有插入成功,也就是发生哈希冲突时也会把哈希桶的首节点赋予p**/
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;
}
六 删除元素
public V remove(Object key) {
Node<K,V> e;
/**调用removeNode(hash(key), key, null, false, true)进行删除,第三个value为null,表示,把key的节点直接都删除了,不需要用到值,如果设为值,则还需要去进行查找操作**/
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**第一参数为哈希值,第二个为key,第三个value,第四个为是为true的话,则表示删除它key对应的value,不删除key,第五个如果为false,则表示删除后,不移动节点**/
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;
//如果未初始化、删除的位置为空 长度小于0 的等直接返回
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;
//找到要删除的节点并赋值给node
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
//对应hash位红黑树查找
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;
}
七 获取元素
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;
//找到对应的位置的hash值
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;
}
八 tableSizeFor
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;
}
作用:该方法是求得大于给定值的最小的是2的整数幂的数。
原理:2的整数幂用二进制表示都是最高有效位为1,其余全是0
核心思想是,先将最高有效位以及其后的位都变为1,最后再+1,就进位到前一位变成1,其后所有的满2变0。所以关键是如何将最高有效位后面都变为1。
基本就是不断的右异运算,再加上或运算,将0移出去,最后再加上1就得到了正确的数。
-
备注:
位移运算符相关知识:https://www.jianshu.com/p/927009730809
来源:https://www.cnblogs.com/xiyixiaodao/p/14483876.html
九 hash
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//具体的使用过程 找到对应的下标 计算结果和 hash % n 结果是一样的
(n - 1) & hash
首先看一下 (n - 1) & hash 这个如何求得的对应数组的位置,使用与运算。这样操作只能获取到长度的二进制的后边的几位数字。
比如:
n=16,则(n-1) = 15 = 01111
hash = 101010 = 42
&的时候高位补0 最终结果是 001010 = 10 = 42%16
再看下扰动函数
从上边可以看到使用的过程也就是保留低位的hash值,但是如果出现两个hash值的低位一样,就会发生hash冲突,所以使用扰动函数进行重新计算,保留高位特性,降低冲突的可能性
十 resize
final Node<K,V>[] resize() {
//老数组
Node<K,V>[] oldTab = table;
//老的长度 未初始化则为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//老的临界值
int oldThr = threshold;
//新长度 新临界值
int newCap, newThr = 0;
//代表已经进行初始化过
if (oldCap > 0) {
//如果老的长度已经到达最大长度则不再进行扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//老的长度左移一位后小于最大长度 临界值也乘以2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//使用 HashMap(int initialCapacity, float loadFactor) 进行初始化的 oldCap是空的,但是oldThr不是
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else {
//首次初始化
// zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//初始化临界值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//这里重新实例化一个数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//实际的扩容操作
if (oldTab != null) {
//循环老数组每个节点进行扩容
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//如果老数组的当前节点不为空则开始迁移
if ((e = oldTab[j]) != null) {
//老节点位置先设置为null,减少引用
oldTab[j] = null;
if (e.next == null)
//如果没有后续的节点则直接迁移,重新计算下标
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//如果当前节点已经是红黑树则走红黑树的迁移
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
//也就是链表的迁移 这里会将链表拆分成两个,拆分的依据是(e.hash & oldCap) == 0
// preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//根据(e.hash & oldCap) == 0 判断进行拆分链表 偏移量是oldCap
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
十一 线程安全问题
1.7的扩容代码如下
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
// 新建一个数组并循环赋值 采用头插法
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
1.7的扩容在并发的情况下会存在可能生成环、
1.8的扩容如上解析,会将链表拆分为两个链表,根据(e.hash & oldCap) == 0,这样会发生数据丢失的风险,一个线程已经将节点异走,但是最终更新table的的是另一个线程。
十二 树化操作
//这里是插入的一段代码,在遍历链表的时候会判断当前链表是否超过TREEIFY_THRESHOLD 否则就会进行树化
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//解除树化 发生在resize阶段 将一个链表拆解为两个
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
十三 常见问题
HashTable和HashMap的区别?
- HashTable是线程安全的,HashMap是线程不安全的。
- HashTable中的value不能为null,否责会抛出空指针异常。
- HashTable扩容时,新数组长度为原数组长度的二倍加一,HashMap是原来的二倍。
- HashTable的扩容方式是遍历数组位值,遍历链表用key的hash值与数组长度直接取余,之后使用头插法插入。HashMap是遍历数组,之后遍历链表生成高低位两个链表,使用的尾插法,之后将两个链表分别放到原位置和原位置加原数组长度位置。
HashTable只使用链表,HashMap在链表长度操作8时会将链表转化为红黑树。
ConcurrentHashMap和HashTable选择?
毫无疑问选择ConcurrentHashMap,HashTable是将方式用synchronized锁住,即使同时插入的键值对对应的数组index不同也会阻塞。ConcurrentHashMap(1.8)是通过锁数组index位置的节点来控制并发,这样即使同时插入,但是如果在数组的index不同也可以同时插入,而且ConcurrentHashMap使用了大量的cas操作,降低了线程在不同状态转化的消耗,ConcurrentHashMap源码解析以后会写。
HashMap数组长度为什么必须是2的整数次幂
数组下标通过hash & (n - 1)来进行计算,当数组长度n为2的正数次幂时,n-1结果的二进制全都是1,这样可以使得&操作的结果更加分散。如果n不是2的幂次,那么减1就可能有的位是0,那么与hash值做&操作,无论hash值的对应位是0还是1都会被散列到同一个位置。
因为hashmap确定键值对位置是通过key的hash值与数组长度减一做与操作来确定,只有数组长度是2的整数次幂才能使(hash & (length-1)) == (hash % length)。
HashMap的hash函数为什么要对高16位做异或操作
这个操作叫做扰动函数,因为这样可以结合hash值的高16位的特性,减少低16位相同高16位不相同的key发生碰撞,减少hash冲突概率。
有什么方法可以减少碰撞?
扰动函数可以减少碰撞
使用String,Interger这样的wrapper类作为键是非常好的选择。
拉链法导致的链表过深问题为什么不用二叉查找树代替
之所以选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构
红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题
解决hash 碰撞还有那些办法
开放定址法
- 当冲突发生时,使用某种探查技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定的地址。
其他
https://blog.csdn.net/QGhurt/article/details/107323702?utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control
- 当冲突发生时,使用某种探查技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定的地址。