HashMap、LinkedHashMap、TreeMap、HashTable、ConcurrentHashMap原理解析
来源:互联网 发布:怎样优化win7开机时间 编辑:程序博客网 时间:2024/05/22 13:11
一、Map家族特点收集
下面,我们从数据结构和算法两个方面一一揭晓每一种Map的实现原理。
二、HashMap
HashMap原理:HashMap使用的数据结构是数组,每次put一个元素时,先计算key的hashCode,然后根据哈希函数找到key在数组中的位置i,如果i位置没有元素,直接插入,否则采用单链表存储。
HashMap的实现原理中,关键在下面两个点:
采用数组作为数据结构
由于选择了数组来存储数据,所以需要解决下面的2个问题:
(1) 数组的初始容量,最大容量
(2)如何扩容使用了哈希算法
在使用哈希算法时,需要解决下面的2个问题:
(1)哈希函数
(2)处理冲突方法
下面,我们从原理角度来分析HashMap如何解决我们上面说的几个问题。
1、数组的初始容量
HashMap中每一个元素使用内部类Entry< K,V>来存储,并且内部维护了一个Entry的数组。
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash;}
数组的初始容量可以通过HashMap的构造方法看出来,HashMap共提供了3个构造方法。
//默认的数组大小是16(HashMap要求数组容量必须为2的幂)。有兴趣的读者,可以考虑一下为什么默认大小是16,而且为什么一定要为2的幂?static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16//最大的数组大小是2^30。static final int MAXIMUM_CAPACITY = 1 << 30;//默认的装载因子是0.75。有兴趣的读者,可以考虑一下为什么默认的装载因子是0.75static final float DEFAULT_LOAD_FACTOR = 0.75f;//数组transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);}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; threshold = initialCapacity; init();}
最核心的方法就是HashMap(int initialCapacity, float loadFactor)中的init()方法,但是查看init()方法时,发现只是一个空实现,那么table数组是在什么时候初始化的呢?只有一种可能,就是第一次put操作时进行初始化的。
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } //......}//初始化数组private void inflateTable(int toSize) { // Find a power of 2 >= toSize //HashMap要求数组容量必须为2的幂,所以需要找到大于等于初始容量的最小的2的幂 int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); //数组初始化 table = new Entry[capacity]; initHashSeedAsNeeded(capacity);}
至于这个里面的装载因子loadFactor与hashSeed,在后面将数组扩容和哈希函数时会讲到。
initHashSeedAsNeeded方法用于初始化hashSeed参数,其中hashSeed用于计算key的hash值,它与key的hashCode进行按位异或运算。这个hashSeed是一个与实例相关的随机值,主要用于解决hash冲突。
2、哈希函数
前面我们讲过,HashMap的容量一定要为2的指数倍(默认是16),这是为什么呢?了解完HashMap中哈希函数的设计原理,你就清楚了。
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);}/*** Returns index for hash code h.*/static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1);}
在哈希表容量为length的情况下,为使key能在冲突最小的情况下映射到[0,length)(注意是左闭右开)内,一般有两种做法:
(1)让length为素数,然后用hashCode(key) mod length
的方法得到数组索引
(2)让length为2的指数倍,然后用hashCode(key) & (length-1)
的方法得到数组索引
HashTable用的是方法1,HashMap用的是方法2。
因为我们讨论的话题是HashMap,所以下面我们重点说一下方法2。方法2不难理解,因为length为2的指数倍,所以length-1的二进制位全部为1,然后再与hashCode(key)
做与运算,就能得到[0,length)内的索引。但是存在一个问题,如果hashCode(key)
大于length 的值,并且低位变化不大,那么冲突就会很多。
HashMap中的hash
函数就是用来解决这个问题的:
首先有一个随机的hashSeed,来降低冲突发生的几率,然后如果是字符串,使用了sun.misc.Hashing.stringHash32((String) k)
来获取索引,最后,通过一系列的无符号右移操作,来把高位与地位进行异或操作,从而降低冲突发生的几率。
至于无符号右移时,为什么使用20、12、7、4,可以参考下面的文章:
https://www.cnblogs.com/killbug/p/4560000.html
3、处理哈希冲突
当不同的key经过哈希函数计算出来的索引相同时,也就是产生冲突时,HashMap使用链地址法来处理冲突。
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex);}void createEntry(int hash, K key, V value, int bucketIndex) { //获取当前的冲突链 Entry<K,V> e = table[bucketIndex]; //将<K,V>放到冲突链的头部。也就是说,后插入的反而在前面 table[bucketIndex] = new Entry<>(hash, key, value, e); size++;}
4、数组扩容
HashMap扩容条件:size ≥ capacity * loadFactor
HashMap扩容算法:容量变为当前容量的2倍
举个简单的例子:初始容量为16,装载因子为0.75,当元素的个数达到12时,HashMap就会将容量扩充到32。
//因为Java中的数组是不能扩容的,所以HashMap使用一个新的数组代替原来的数组,然后所有元素重新计算索引,插入到新数组中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; //遍历当前的table,将里面的元素添加到新的newTable中 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; } }}
三、LinkedHashMap
LinkedHashMap继承自HashMap,与HashMap唯一的区别就在于LinkedHashMap是有序的,而HashMap是无序的。
在理解了HashMap的基础上,再来看LinkedHashMap的原理,就很简单的,只需要弄清楚LinkedHashMap是怎么实现有序的就可以了。
为了保证有序性,LinkedHashMap主要做了以下几方面的改造:
- 将Entry设计为双向循环链表
- 重写addEntry和createEntry方法
- 添加accessOrder,用于标识是按插入顺序还是访问顺序排序
1、将Entry设计为双向循环链表
LinkedHashMap.Entry
继承自HashMap.Entry
,并且添加了before和after两个指针,分别指向前继节点与后继节点
private static class Entry<K,V> extends HashMap.Entry<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, HashMap.Entry<K,V> next) { super(hash, key, value, next); } //删除一个节点时,前继节点的后继指针指向要删除节点的后继节点,后继节点的前继指针指向要删除节点的前继节点 private void remove() { before.after = after; after.before = before; } //在existingEntry节点前插入节点 private void addBefore(Entry<K,V> existingEntry) { after = existingEntry; before = existingEntry.before; before.after = this; after.before = this; } //如果是按照插入的顺序排序,什么都不干 void recordAccess(HashMap<K,V> m) { LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m; if (lm.accessOrder) { lm.modCount++; remove(); addBefore(lm.header); } } void recordRemoval(HashMap<K,V> m) { remove(); }}
2、重写addEntry和createEntry方法
LinkedHashMap的put方法直接继承自HashMap,下面是put方法的核心流程:
put->addEntry ->createEntry
void addEntry(int hash, K key, V value, int bucketIndex) { super.addEntry(hash, key, value, bucketIndex); Entry<K,V> eldest = header.after; if (removeEldestEntry(eldest)) { removeEntryForKey(eldest.key); }}void createEntry(int hash, K key, V value, int bucketIndex) { //创建新的Entry,并将其插入到数组对应槽的单链表的头结点处,这点与HashMap中相同 HashMap.Entry<K,V> old = table[bucketIndex]; Entry<K,V> e = new Entry<>(hash, key, value, old); table[bucketIndex] = e; //每次插入Entry时,都将其移到双向链表的尾部,这便会按照Entry插入LinkedHashMap的先后顺序来迭代元素, //同时,新put进来的Entry是最近访问的Entry,把其放在链表末尾 ,符合LRU算法的实现 e.addBefore(header); size++;}
3、添加accessOrder,用于标识是按插入顺序还是访问顺序排序
LinkedHashMap提供了下面两种排序模式:
- accessOrder为false,表示按插入顺序排序。
- accessOrder为true,表示按访问顺序排序。
从构造函数可以看出,LinkedHashMap默认按照插入顺序排序。
public LinkedHashMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); accessOrder = false;}public LinkedHashMap(int initialCapacity) { super(initialCapacity); accessOrder = false;}//使用该构造函数可以自定义排序模式public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder;}
按照插入顺序排序比较好理解,唯一需要注意的就是同一个key多次进行put操作时,顺序会不会改变。
查看HashMap.put()
的源码,可以看出,在插入的key已存在的情况下,会调用Entry.recordAccess()
方法,在插入的key不存在的情况下,要调用addEntry插入新的Entry 。Entry.recordAccess()
方法在accessOrder =false时,什么都不干,所以同一个key多次进行put操作时,顺序是不会改变的。
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; //在插入的key已存在的情况下,调用Entry.recordAccess方法 e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null;}
当accessOrder =true时,也就是按照访问顺序排序时,LinkedHashMap采用的是LRU(Least recently used,最近最少使用)算法。
与LRU算法的常规实现不同的是,如果某个key最近被访问过,那么LinkedHashMap认为它后面访问的几率更低。所以,当某个key被访问后,会将它移动到双向循环链表的尾部。
主要体现在下面两个操作:
- get操作
- put操作
public V get(Object key) { Entry<K,V> e = (Entry<K,V>)getEntry(key); if (e == null) return null; //如果链表中元素的排序规则是按照访问的先后顺序排序的话,则将e移到链表的末尾处。 e.recordAccess(this); return e.value; }//put操作体现在createEntry方法中,每次将新加入的节点放在双向链表尾部//……
- HashMap、LinkedHashMap、TreeMap、HashTable、ConcurrentHashMap原理解析
- HashMap、HashSet、Hashtable、concurrentHashmap、treemap、linkedhashmap对比
- LinkedHashMap、HashMap、HashTable、TreeMap、ConcurrentHashMap的比较
- HashMap Hashtable LinkedHashMap TreeMap
- HashMap 、TreeMap、Hashtable、LinkedHashMap
- HashMap HashTable LinkedHashMap TreeMap
- HashMap,HashTable,LinkedHashMap,TreeMap
- hashMap,hashTable,linkedHashMap,TreeMap
- 从源代码看TreeMap、HashMap、Hashtable、ConcurrentHashMap、LinkedHashMap特性
- HashMap、Hashtable、LinkedHashMap、TreeMap、ConcurrentHashMap之间的区别-yellowcong
- HashMap Hashtable LinkedHashMap 和TreeMap
- HashMap Hashtable LinkedHashMap 和TreeMap
- HashMap Hashtable LinkedHashMap 和TreeMap
- HashMap、Hashtable、LinkedHashMap和TreeMap
- HashMap,LinkedHashMap,HashTable,TreeMap 区别
- HashMap Hashtable LinkedHashMap 和TreeMap
- HashMap、HashTable、LinkedHashMap和TreeMap
- HashMap Hashtable LinkedHashMap 和TreeMap
- React 组件通信
- 【HDU】 1213
- Java练习题14 教师研究生工资
- 音频常用的音频格式
- 用Condtruct2 制作游戏2
- HashMap、LinkedHashMap、TreeMap、HashTable、ConcurrentHashMap原理解析
- java中的快速失败(fail-fast)与安全失败(fail-safe)
- 特斯拉为什么要造电动卡车?
- 网狐荣耀版手机端在复制文本时提示“复制内容非法”的解决办法
- 共享自己电脑硬盘空间还能赚钱?
- Protues8示波器的使用
- 128. Longest Consecutive Sequence
- 循环与回调函数
- 让而莫争 放而莫贪