一 特性
- 使用hash方法进行区分key
- 可以存在一个为null的key
- 线程不安全
- HashMap在多线程情况下,rehash过程中有可能导致Entry产生环形链
-
二 属性
//默认容量 16static 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 1sttreeifyBin(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 keyV 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;//找到要删除的节点并赋值给nodeif (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;//老的长度 未初始化则为0int 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;}//老的长度左移一位后小于最大长度 临界值也乘以2else 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 thresholdnewCap = oldThr;else {//首次初始化// zero initial threshold signifies using defaultsnewCap = 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 orderNode<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 判断进行拆分链表 偏移量是oldCapif ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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 1sttreeifyBin(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 orderTreeNode<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;elseloTail.next = e;loTail = e;++lc;}else {if ((e.prev = hiTail) == null)hiHead = e;elsehiTail.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
- 当冲突发生时,使用某种探查技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定的地址。
