hashmap在java里是出现频率较高的类,不管是工作还是面试,掌握hashmap的原理是很重要的,本文也将从整体到细节介绍hashmap
摘要
hashmap在java里是出现频率较高的类,不管是工作还是面试,掌握hashmap的原理是很重要。随着JDK版本的更新,HashMap底层的实现进行了优化,例如引入红黑树的数据结构和扩容的优化等。本文结合JDK1.7和JDK1.8的区别,深入探讨HashMap的结构实现和功能原理。
简介
java中Map数据结构定义了一个主要的接口:java.util.Map。主要实现这个接口的类是:HashMap、HashTable、LinkedHashMap、TreeMap。关系如下
下面针对各个实现类的特点做一些说明:
(1) HashMap:
- 访问速度快hashcode直接定位,但遍历顺序却是不确定的。
- 最多只允许一条记录的键为null,允许多条记录的值为null。
- 非线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。
(2) Hashtable:没什么卵用,基本上与HashMap类似、虽然线程安全,但介于HashMap与ConcurrentHashMap之间,不上不下。
(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序。
(4) TreeMap:
- 它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。
- 如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
本文主要讲HashMap的实现原理,结合1.7和1.8主要从存储结构,常用方法,定位,扩容等方面展开
存储结构
存储结构如图:
HashMap在1.7中只用到了数组和链表,代码也只有一千多行。上图展示的是1.8的存储结构,在1.8中加入了红黑树,在链表大于8的时候转换为红黑树;扩容后导致红黑树节点在小于6时,又会转换成链表。代码量虽然翻倍了,带来的确实性能的提升。
HashMap 1.8结构代码如下
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
| static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; static final int MAXIMUM_CAPACITY = 1 << 30; static final float DEFAULT_LOAD_FACTOR = 0.75f; static final int TREEIFY_THRESHOLD = 8; static final int UNTREEIFY_THRESHOLD = 6; static final int MIN_TREEIFY_CAPACITY = 64;
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; ··· }
transient Node<K,V>[] table; transient Set<Map.Entry<K,V>> entrySet; transient int size; transient int modCount; int threshold; final float loadFactor;
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); }
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; }
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; boolean red; ··· }
|
实现说明
主要从hashmap的主要三个步骤进行说明,hash定位,插入,扩容
hash定位
hash定位是HashMap比较核心的方法了,上面我们了解到HashMap的结构为数组,既然是数组,就会有下标,那么这个hash值就是数组的下标,也正是HashMap可以根据key快速查找定位到Value的原因
下面看下1.7中hash的源码
1 2 3 4 5 6 7 8 9 10 11 12 13
| final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); }
h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } static int indexFor(int h, int length) { return h & (length-1); }
|
在1.8中做了改进
1 2 3 4
| static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
|
1.8中虽然取消了indexFor函数,但是在put和get的时候都通过了 tab[i = (n - 1) & hash] 来定位,原理跟1.7是一样的
可以看出,不管是哪个版本,算法大致分为 取key的hashcode,高位运算 取模运算
为了让hash值均匀分布,会采用高位运算,让小的值的高位也参与运算;然后拿运算后key的hashcode对数组的长度进行模运算定位数组中的位置,但是细心一点就会发现,取模运算并没有使用%运算,
因为模运算是很耗费性能的,所以采用与运算,可以说这个与运算设计的是相当精巧了。这也就是为什么数组的长度一定要是2的N次幂长度的原因,因为当length等于2的n次幂时,h&(length-1)就等于h%length
put实现
我们知道HashMap的时间复杂度为O(1),但是当Hash碰撞率过高时hashmap就会遍历链表,导致某些情况时间复杂度提高至O(n);所以好的hash算法以及扩容机制是相当重要的,下面就讲讲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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| 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; }
|
相比较于1.7的插入操作,1.8的优化是引入红黑树,不至于在hash碰撞频繁的情况下,导致链表过长查询速度变慢的问题。
在插入操作里最后就是扩容函数,想必很想知道hashmap是怎么扩容的,下面详细讲讲扩容原理
扩容
resize就是更换容器,小桶放不下了得换个大桶。前面我们了解到,table是个数组,我们也知道数组是有大小的,不能动态的扩张,但是HashMap对象却可以不停的添加元素,这也真是resize帮我们做的,
方法就是使用一个新的数组代替已有的容量小的数组。下面我们分析下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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| 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; }
|
这跟java 7是完全不同的,java 7是采用遍历后重新hash的方法,并且链表采用的是头插法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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; } } }
|
--------------------------------------
文章到此就结束了,有疑问可以下方评论--------------------------------------
赞赏支持
大部分文章都是自创,如您觉得文章不错,对您有所帮助,转载时请注明出处,也可以通过“支付宝”或“微信支付”请我喝咖啡。
------------------------------------------------------------------------------------------------------------------------------
支付宝打赏二维码
------------------------------------------------------------------------------------------------------------------------------
微信打赏二维码
大部分文章都是自创,如您觉得文章不错,对您有所帮助,转载的时候请注明出处,也可以通过“支付宝”或“微信支付”请我喝咖啡。