刚刚看到QQ群有人吹Hashmap,一想我啥都不懂,就赶快补了一波。下面分享一下我对Hashmap的理解,主要用于我的备忘。若是有不对,请批评。想要解锁更多新姿式?请访问http://blog.tengshe789.tech/node
总起算法
Hashmap是散列表,存储结构是键值对形式。根据健的Hashcode值存储数据,有较快的访问速度。segmentfault
它的线程是不安全的,在两个线程同时尝试扩容HashMap时,可能将一个链表造成环形的链表,全部的next都不为空,进入死循环;要想让它安全,能够用 Collections的synchronizedMap 方法使 HashMap具备线程安全的能力,或者使用ConcurrentHashMap 。数组
他的键值对均可觉得空,映射不是有序的。安全
Hashmap有两个参数影响性能:初始容量,加载因子。数据结构
Hashmap存储结构架构
JDK1.8中Hashmap是由链表、红黑树、数组实现的app
//用来实现数组、链表的数据结构 static class Node<K,V> implements Map.Entry<K,V> { final int hash;//保存节点的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; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry<?,?> e = (Map.Entry<?,?>)o; if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue())) return true; } return false; } }
Hashmap构造方法函数
HashMap有4个构造方法。性能
代码:
//方法1.制定初始容量和负载因子 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } //方法2.指定初始容量 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } //方法三。无参构造。 HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } //方法四。将另外一个 Map 中的映射拷贝一份到本身的存储结构中来,这个方法不是很经常使用 public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
Hashmap变量成员
//未指定容量的时候,数组的初始容量。初始容量是16 //为何不直接写16?由于速度快。计算机里面要转换二进制。 //必须2的n次幂 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 //负载因子。当hashmap容量超过 容量*负载因子 时,进行扩容操做(resize()) static final float DEFAULT_LOAD_FACTOR = 0.75f; //肯定什么时候将hash冲突的链表转换成红黑树 static final int TREEIFY_THRESHOLD = 8; //用来确什么时候将红黑树转换成链表 static final int UNTREEIFY_THRESHOLD = 6; //当链表转换成红黑树时,须要判断数组容量。若数组容量过小致使hash冲突太多,则不进行红黑树操做,转而利用reseize扩容 static final int MIN_TREEIFY_CAPACITY = 64;
初始容量、负载因子、阈值.
通常状况下,使用无参构造方法建立 HashMap。但当咱们对时间和空间复杂度有要求的时候,使用默认值有时可能达不到咱们的要求,这个时候咱们就须要手动调参。
在 HashMap 构造方法中,可供咱们调整的参数有两个,一个是初始容量initialCapacity,另外一个负载因子loadFactor。经过这两个设定这两个参数,能够进一步影响阈值大小。但初始阈值 threshold 仅由initialCapacity 通过移位操做计算得出。
名称 用途
initialCapacity HashMap 初始容量
loadFactor 负载因子
threshold 当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容
默认状况下,HashMap 初始容量是16,负载因子为 0.75。 注释中有说明,阈值可由容量乘上负载因子计算而来 ,即threshold = capacity * loadFactor
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; }
这段代码有点难,根据大神的说法,这个方法的意思是,找到大于或等于 cap 的最小2的幂。咱们先来看看 tableSizeFor 方法的图解 :
图中容量是229+1,计算后是230
引用一下啊大神说的:
对于 HashMap 来讲,负载因子是一个很重要的参数,该参数反应了 HashMap 桶数组的使用状况(假设键值对节点均匀分布在桶数组中)。经过调节负载因子,可以使 HashMap 时间和空间复杂度上有不一样的表现。当咱们调低负载因子时,HashMap 所能容纳的键值对数量变少。扩容时,从新将键值对存储新的桶数组里,键的键之间产生的碰撞会降低,链表长度变短。此时,HashMap 的增删改查等操做的效率将会变高,这里是典型的拿空间换时间。相反,若是增长负载因子(负载因子能够大于1),HashMap 所能容纳的键值对数量变多,空间利用率高,但碰撞率也高。这意味着链表长度变长,效率也随之下降,这种状况是拿时间换空间。至于负载因子怎么调节,这个看使用场景了。通常状况下,咱们用默认值就能够了。
插入PUT
过程:
若是桶满了(容量*负载因子),则从新resize
public V put(K key, V value) {
//调用核心方法 return putVal(hash(key), key, value, false, true); }
putVal
核心算法在putVal()中。要想理解,先要明白桶排序(Bucket Sort)
它是迄今为止最快的一种排序法,其时间复杂度仅为Ο(n),也就是线性复杂度。
桶排序核心思想是:根据数据规模n划分,m个相同大小的区间 (每一个区间为一个桶,桶可理解为容器) 。将n个元素按照规定范围分布到各个桶中去 ,再对每一个桶中的元素进行排序,排序方法可根据须要,选择快速排序,或者归并排序,或者插入排序 ,而后依次从每一个桶中取出元素,按顺序放入到最初的输出序列中(至关于把全部的桶中的元素合并到一块儿) 。
下面是代码:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { //n是数组长度 Node<K,V>[] tab; Node<K,V> p; int n, i; //判断桶数组是不是空 if ((tab = table) == null || (n = tab.length) == 0) //是就用resize()初始化 n = (tab = resize()).length; //根据 hash 值肯定节点在数组中的插入位置 //若此位置没有元素则进行插入,注意肯定插入位置所用的计算方法为 (n - 1) & hash,因为 n 必定是2的幂次,这个操做至关于hash % n if ((p = tab[i = (n - 1) & hash]) == null) //将新节点引入桶中 tab[i] = newNode(hash, key, value, null); else { //临时变量e进行记录。若是有值,说明仅仅是值的覆盖。 Node<K,V> e; K k; // 若是键的值以及节点 hash 等于链表中的第一个键值对节点时,则将 e 指向该键值对 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode)// 若是桶中的引用类型为 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; } } //临时变量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; }
HASH
hash算法,高十六位与低十六进行异或运算,这样作的好处是使获得结果会尽量不一样。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
resize
HashMap 的扩容机制与其余变长集合的套路不太同样,HashMap 按当前桶数组长度的2倍进行扩容,阈值也变为原来的2倍(若是计算过程当中,阈值溢出归零,则按阈值公式从新计算)。扩容以后,要从新计算键值对的位置,并把它们移动到合适的位置上去。
resize总共作了3件事,分别是:
将键值对节点从新映射到新的桶数组里。若是节点是 TreeNode 类型,则须要拆分成黑树。若是是普通节点,则节点按原顺序进行分组。
//resize()函数在size > threshold时被调用
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; //oldCap大于 0 表明原来的 table 非空 if (oldCap > 0) { // 当 table 容量超过容量最大值,则再也不扩容 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 } else if (oldThr > 0) // initial capacity was placed in threshold /* *oldCap 小于等于 0 且 oldThr 大于0,表明用户建立了一个 HashMap,可是使用的构造函数为 * HashMap(int initialCapacity, float loadFactor) 或 HashMap(int initialCapacity) * 或 HashMap(Map<? extends K, ? extends V> m),致使 oldTab 为 null,oldCap 为0, * oldThr 为用户指定的 HashMap的初始容量 */ newCap = oldThr; else { //设置新容量和新阈值大小 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // newThr 为 0 时,按阈值计算公式进行计算 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) { //遍历。把 oldTab 中的节点 reHash 到 newTab 中去 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; //判断老数组是否为空 if ((e = oldTab[j]) != null) { //不为空,设成空 oldTab[j] = null; //若节点是单个节点,直接从新分配定位 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //若节点是 TreeNode 节点,要进行 红黑树的 rehash 操做 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //如果链表,进行链表的 rehash 操做 else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; // 遍历链表,并将链表节点按原顺序进行分组 do { next = e.next; 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; // rehash 后节点新的位置必定为原来基础上加上 oldCap newTab[j + oldCap] = hiHead; } } } } } return newTab;
}
关于HashMap在何时时间复杂度是O(1),何时是O(n),何时又是O(logn)的问题
O(1):链表的长度尽量短,理想状态下链表长度都为1
O(n):当 Hash 冲突严重时,若是没有红黑树,那么在桶上造成的链表会变的愈来愈长,这样在查询时的效率就会愈来愈低;时间复杂度为O(N)。
O(logn):采用红黑树以后能够保证查询效率O(logn)
手写
/** * @author tengshe789 */ public class 手写HashMap { public static class Node<K,V>{ K key; V value; Node<K,V> next; public Node(K key, V value, Node<K, V> next) { this.key = key; this.value = value; this.next = next; } public K getKey() { return this.key; } public V getValue() { return this.value; } public V setValue(V value) { this.value=value; return this.value; } } public static class HashMap<K, V>{ /*数据存储的结构==>数组+链表*/ Node<K,V>[] array=null; /* 哈希桶的长度 */ private static int defaultLength=16; /*加载因子/扩容因子*/ private static double factor=0.75D; /*集合中的元素个数*/ private int size; /*打印函数*/ public void print() { System.out.println("==============================="); if(array!=null) { Node<K, V> node=null; for (int i = 0; i < array.length; i++) { node=array[i]; System.out.print("下标["+i+"]"); //遍历链表 while(node!=null) { System.out.print("["+node.getKey()+":"+node.getValue()+"]"); if(node.next!=null) { node=node.next; }else { //到尾部元素 node=null; } } System.out.println(); } } } //put元素方法 public V put(K k, V v) { //1.懒加载机制,使用的时候进行分配 if(array==null) { array=new Node[defaultLength]; } //2.经过hash算法,计算出具体插入的位置 int index=position(k,defaultLength); //扩容。判断是否须要扩容 //扩容的准则,元素的个数 大于 桶的尺寸*加载因子 if(size > defaultLength*factor) { resize(); } //3.放入要插入的元素 Node<K, V> node=array[index]; if(node==null) { array[index]=new Node<K,V>(k,v,null); size++; }else { if(k.equals(node.getKey()) || k==node.getKey()) { return node.setValue(v); }else { array[index]=new Node<K,V>(k,v,node); size++; } } return null; } //扩容,而且从新排列元素 private void resize() { //翻倍扩容 //1.建立新的array临时变量,至关于defaultlength*2 Node<K, V>[] temp=new Node[defaultLength << 1]; //2.从新计算散列值,插入到新的array中去。 code=key % defaultLength ==> code=key % defaultLength*2 Node<K, V> node=null; for (int i = 0; i < array.length; i++) { node=array[i]; while(node!=null) { //从新散列 int index=position(node.getKey(),temp.length); //插入头部 Node<K, V> next = node.next; //3 node.next=temp[index]; //1 temp[index]=node; //2 node=next; } } //3.替换掉老array array=temp; defaultLength=temp.length; temp=null; } private int position(K k,int length) { int code=k.hashCode(); //取模算法 return code % (length-1); //求与算法 //return code & (defaultLength-1); } public V get(K k) { if(array!=null) { int index=position(k,defaultLength); Node<K, V> node=array[index]; //遍历链表 while(node!=null) { //若是key值相同返回value if(node.getKey()==k) { return node.getValue(); } else //若是key值不一样则调到下一个元素 { node=node.next; } } } return null; } } public static void main(String[] args) { HashMap<String, String> map=new HashMap<String, String>(); map.put("001号", "001"); map.put("002号", "002"); map.put("003号", "003"); map.put("004号", "004"); map.put("005号", "005"); map.put("006号", "006"); map.put("007号", "007"); map.put("008号", "008"); map.put("009号", "009"); map.put("010号", "010"); map.put("011号", "011"); map.print(); System.out.println("========>"+map.get("009号")); } }
参考资料
coolblog
阿里架构师带你分析HashMap源码实现原理
感谢!
如下来自n天后的我:
补充一下看到一个很是好的:点击连接,值得学习
想要了解更多精彩新姿式?请访问个人我的博客 本篇为原创内容,已在我的博客率先发表,随后CSDN,segmentfault,juejin同步发出。若有雷同,那真是缘分~