HashMap原理 HashMap怎么使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import java.util.HashMap;import java.util.Map;public class test { public static void main (String[] args) { Map<Integer,String> map=new HashMap<Integer,String> (16 ); System.out.println("map加入值" ); map.put(1 ,"q" ); map.put(2 ,"w" ); map.put(3 ,"e" ); System.out.println("判断该键是否存在:" +map.containsKey(4 )); System.out.println("获取该键的值:" +map.get(1 )); System.out.println("map的大小:" +map.size()); System.out.println("判断是否相等:" +"q" .equals(map.get(1 ))); for (String a: map.values()) { System.out.print(a); } } }
HashMap的使用比较直观,可以粗略的把HashMap看做为一个数组去使用。
HashMap的定义 从类的定义来看
1 2 public class HashMap <K ,V > extends AbstractMap <K ,V > implements Map <K ,V >, Cloneable , Serializable
HashMap继承了抽象map类,实现了cloneable和Serializable接口。
HashMap的数据都存在一个entry数组里面,在HashMap有一个静态类去实现。
1 2 3 4 5 6 7 8 9 10 11 12 static class Entry <K ,V > implements Map .Entry <K ,V > { final K key; V value; Entry<K,V> next; int hash; static class Node <K ,V > implements Map .Entry <K ,V > { final int hash; final K key; V value; Node<K,V> next;
可以看到无论是JDK7还是JDK8,都定义了两种值,key和value,我们存入的数据,都是存入在这数组之中,而且还定义了一个next,next就像是链表一样,用于指向下一个值。所以table中存储的是Entry的单向链表。默认参数的HashMap结构如下图所示:
所以,可以把它看作为是一个数组,然后每个数组都由链表组成。
HashMap的构造方法 HashMap一共有四个构造方法,这里只看一下默认的构造方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public HashMap (int initialCapacity) { this (initialCapacity, DEFAULT_LOAD_FACTOR); } 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); } 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 ; }
哈希桶数组会在首次使用时初始化,默认大小是 16,并根据需要调整大小,且长度总是 2 的次幂。如果构造函数设置的初始容量不是 2 的次幂,那么使用tableSizeFor方法,来设置一个大于且靠近它的 2 的次幂的值。
影响 HashMap 性能的主要参数是:初始容量 和负载因子 。当散列表元素数超过负载因子和当前容量的乘积时((initialCapacity * loadFactor)。),就会扩容,扩大到原来容量的两倍 ,并对键重新散列。
HashMap 内部的其他字段:
1 2 3 4 5 6 7 8 transient int size;transient int modCount;final float loadFactor;int threshold;
HashMap的put()方法 put方式是我们在使用HashMap中经常使用的方法,所以要好好研究。
1 2 3 public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); }
可以看到,我们在使用put方法的时候,实际是使用一个putVal方法。而在这个putVal方法中,会将key值先进行hash运算,得到hash码。
hash()方法 这个hash方法如下:
1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
如果key值为null的时候,就返回hash码为0。但如果不为null呢?
1 (h = key.hashCode()) ^ (h >>> 16);
这个会是什么意思呢?这段代码叫“扰动函数 ”。key.hashCode() 函数调用的是key键值类型自带的哈希函数,返回int型散列值。这个求出hashcode的方法非常的复杂。
1 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
理论上散列值是一个int型,如果直接拿散列值作为下标访问HashMap主数组的话,考虑到2进制32位带符号的int表值范围从-2147483648 到2147483648 。前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。
但问题是一个40亿长度的数组,内存是放不下的。你想,HashMap扩容之前的数组初始大小才16。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组下标。
而这个对数组的长度取模运算的操作,正好解释了为什么HashMap的数组长度要取2的整次幂。因为这样(数组长度-1 )正好相当于一个“低位掩码”。
但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重。更要命的是如果散列本身做得不好,分布上成等差数列的漏洞,恰好使最后几个低位呈现规律性重复,就无比蛋疼。
这时候“扰动函数 ”的价值就体现出来了:
右位移16位,正好是32bit的一半,自己的高半区和低半区做异或,就是为了混合原始哈希码的高位和低位,以此来加大低位的随机性 。而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。
此时的key值先求出hash值,再经过扰动函数之后,得出来的hash值的重复率就大大的降低了。而对数组的长度取模运算的操作,发生在putVal()方法当中。
putVal()方法 我们可以从源码上得之,使用put方法,实际上是在使用putVal方法,那我们来解析一下吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 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 ) n = (tab = resize()).length; 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 ) treeifyBin(tab, hash); break ; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break ; p = e; } } if (e != null ) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null ) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null ; }
PutVal方法中,会对Hash码和hashmap的长度-1做与运算,这样的运算方式比取余更为快速。从而得到的最后结果就是元素存放的地点。
HashMap的扩容:resize() JDK8的优化扩容机制 每次在空的数组中存放元素成功,就会执行++size操作,当元素存储的大小大于threshold,即大于整个数组的0.75倍时,就会触发扩容操作,把整个数组扩容成原来大小的两倍 。因为使用的是2的次幂扩展 ,那么元素的位置要么保持不变,要么在原位置上偏移2的次幂。
看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。
我们接下来看看JDK8的resize()的源码实现吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; 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; } else if ((newCap = oldCap << 1 ) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1 ; } else if (oldThr > 0 ) newCap = oldThr; else { 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 ) { 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 { 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 ; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
链表转红黑树 链表转红黑树主要做了以下几件事:
判断桶容量是否达到树化的最低要求,否则进行扩容
将原链表转为由 TreeNode 组成的双向链表
将新链表转为红黑树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 final void treeifyBin (Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1 ) & hash]) != null ) { TreeNode<K,V> hd = null , tl = null ; do { TreeNode<K,V> p = replacementTreeNode(e, null ); if (tl == null ) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null ); if ((tab[index] = hd) != null ) hd.treeify(tab); } } TreeNode<K,V> replacementTreeNode (Node<K,V> p, Node<K,V> next) { return new TreeNode<>(p.hash, p.key, p.value, next); }
HashMap 在设计时应该没有考虑后期会引入红黑树,所以没有提供 key 的比较器或要求 key 实现 Comparable 接口。为了比较两个 key 的大小,HashMap 按以下步骤处理:
如果两个 key 的 hash 值不等,则比较 hash 值大小
如果相等,若 key 实现了 Comparable 接口,使用 compareTo 方法比较
如果结果还是相等,使用自定义的 tieBreakOrder 方法比较,逻辑如下
1 2 3 4 5 6 7 8 static int tieBreakOrder (Object a, Object b) { int d; if (a == null || b == null || (d = a.getClass().getName().compareTo(b.getClass().getName())) == 0 ) d = (System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1 ); return d; }
优化总结 JDK8 中的 HashMap 代码还是比较复杂的,优化方面主要有以下三点:
优化 hash 算法只进行一次位移操作
引入红黑树,在冲突比较严重的情况下,将 get 操作的时间复杂从 O(n) 降为了 O(logn)
扩容时,利用 2 的次幂数值的二进制特点,既省去重新计算 hash 的时间,又把之前冲突的节点散列到了其他位置
此外,HashMap 是非线程安全 的,线程间的竞争条件 主要是发生冲突或扩容时,链表的断链和续链操作。扩容也就意味着内存拷贝,这是一个很耗费性能的操作,所以预分配一个足够大的初始容量,减少扩容的次数,能够让 HashMap 有更好的表现。
HashMap的get()方法 get()方法也是在HashMap一种较为常用的方法,我们来看看它是怎么回事吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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 && ((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值定位到哈希桶的索引,当可以就是当前索引的值则直接返回其对于的value,反之用key去遍历equal该索引下的key,直到找到位置。如图所示:
HashMap的remove()方法 最后还有一个基本的方法,那就是remove方法了。
1 2 3 4 5 public V remove (Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null , false , true )) == null ? null : e.value; }
这个remove方法实际上调用的是removeNode方法,而它的参数和putVal方法中非常的类似。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 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 ; }
删除操作就是一个查找+删除的过程,它的步骤也很简单,就先根据hashcode值,找到bucket的位置,找到位置之后,在节点中根据key的值,找到对应的value。
总结
HashMap是基于哈希表实现的,用Entry[]来存储数据,而Entry中封装了key、value、hash以及Entry类型的next
HashMap存储数据是无序的
hash冲突是通过拉链法解决的
HashMap的容量永远为2的幂次方,有利于哈希表的散列
HashMap不支持存储多个相同的key,且只保存一个key为null的值,多个会覆盖
put过程,是先通过key算出hash,然后用hash算出应该存储在table中的index,然后遍历table[index],看是否有相同的key存在,存在,则更新value;不存在则插入到table[index]单向链表的表头,时间复杂度为O(n)
get过程,通过key算出hash,然后用hash算出应该存储在table中的index,然后遍历table[index],然后比对key,找到相同的key,则取出其value,时间复杂度为O(n)
方法摘要 void clear() 从此映射中移除所有映射关系。 Object clone() 返回此 HashMap 实例的浅表副本:并不复制键和值本身。 boolean containsKey(Object key) 如果此映射包含对于指定键的映射关系,则返回 true。 boolean containsValue(Object value) 如果此映射将一个或多个键映射到指定值,则返回 true。 Set<Map.Entry<K,V>> entrySet() 返回此映射所包含的映射关系的 Set 视图。 V get(Object key) 返回指定键所映射的值;如果对于该键来说,此映射不包含任何映射关系,则返回 null。 boolean isEmpty() 如果此映射不包含键-值映射关系,则返回 true。 Set keySet() 返回此映射中所包含的键的 Set 视图。 V put(K key, V value) 在此映射中关联指定值与指定键。 void putAll(Map<? extends K,? extends V> m) 将指定映射的所有映射关系复制到此映射中,这些映射关系将替换此映射目前针对指定映射中所有键的所有映射关系。 V remove(Object key) 从此映射中移除指定键的映射关系(如果存在)。 int size() 返回此映射中的键-值映射关系数。 Collection values() 返回此映射所包含的值的 Collection 视图。