HashMap
HashMap
是一个基于哈希表的实现,可以提供键-值映射.上次巴神指点我应该去读读它的源码,所以今天就来看看它心里是怎么想的.
第一行
阅读源码首先就要看看第一行咯,看看它继承了谁,可以给它定性.再看看它实现的接口,可以知晓它能够拥有哪些动作.
1 2 3 | public class HashMap<K, V> extends AbstractMap<K, V> implements Cloneable, Serializable{ ... } |
对于AbstractMap<K, V>
,显然是一个抽象类
1 2 3 | public abstract class AbstractMap<K, V> implements Map<K, V> { ... } |
Map
对于Map<K, V>
,首先是一个叫Entry的接口,显而易见,这就是map的存储结构,它的每一个元素都是这样的.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /** * {@code Map.Entry} is a key/value mapping contained in a {@code Map}. */ public static interface Entry<K,V> { //比较传过来的object对象和当前的 entry对象的hashCode是否相等,如果相等就返回true,否则返回false public boolean equals(Object object); //返回key public K getKey(); //返回Value public V getValue(); //获取hashCode 是一个int值 public int hashCode(); //将这个entry的值设为指定的值(object) public V setValue(V object); }; |
然后就是一系列方法
方法 | 作用 |
clear() | 清除所有entrty |
containsKey(Object key) | 检查Map是否包含key |
containsValue(Object value) | 检查Map是否包含Value |
entrySet() | 返回一个Set<map.entry>集合,这个集合包含了Map所有的Entry</map.entry |
equals(Object object) | 判断Map集合是否与指定的对象相同 |
get(Object key) | 通过key得到Value |
isEmpty() | 判断Map是否为空 |
keySet() | 返回一个Set集合,包含了所有的key |
put(K key, V value) | 将指定的key和value映射到map里 |
putAll(Map<? extends K,? extends V> map) | 将指定map里的数据copy到当前map |
remove(Object key) | 删除指定key的映射 |
size() | 返回映射的个数 |
values() | 返回map中所有value的集合 |
HashMap#HashMapEntry
HashMap的爷爷是Map,所以自然也继承了Entry,构造方法包括四个参数
o key 你懂的
o value 你懂的
o hash 当前映射的hash
o next 下一个entry
hash相同的entry将会被放到一个链表里,next指向下一个entry.
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 | static class HashMapEntry<K, V> implements Entry<K, V> { final K key; V value; final int hash; HashMapEntry<K, V> next; HashMapEntry(K key, V value, int hash, HashMapEntry<K, V> next) { this.key = key; this.value = value; this.hash = hash; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final V setValue(V value) { V oldValue = this.value; this.value = value; return oldValue; } @Override public final boolean equals(Object o) { if (!(o instanceof Entry)) { return false; } Entry<?, ?> e = (Entry<?, ?>) o; return Objects.equal(e.getKey(), key) && Objects.equal(e.getValue(), value); } @Override public final int hashCode() { return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode()); } @Override public final String toString() { return key + "=" + value; } } |
三个构造方法
无参构造
1 2 3 4 5 | @SuppressWarnings("unchecked") public HashMap() { table = (HashMapEntry<K, V>[]) EMPTY_TABLE; threshold = -1; // Forces first put invocation to replace EMPTY_TABLE } |
以此方法可以构造一个空的HashMap;
如果散列表包含空映射,table就被赋值new HashMapEntry[MINIMUM_CAPACITY >>> 1];
这个是啥呢?MINIMUM_CAPACITY
不用说,就是最小容量了,是4.MINIMUM_CAPACITY>>>1
这里的>>>
是无符号右移,忽略了符号位扩展,0补最高位,所以就是4>>>1=2;
所以这里 Entry[] EMPTY_TABLE = new HashMapEntry[2];
threshold
:这里赋值为-1 其实就是让它成为一个新的表,当表size大于它时表将会被重新hash.
两个参数
1 2 3 4 5 6 7 8 9 10 11 12 13 | public HashMap(int capacity, float loadFactor) { this(capacity); if (loadFactor <= 0 || Float.isNaN(loadFactor)) { throw new IllegalArgumentException("Load factor: " + loadFactor); } /* * Note that this implementation ignores loadFactor; it always uses * a load factor of 3/4. This simplifies the code and generally * improves performance. */ } |
两个参数
o capacity:指定初始化容量你懂的
o loadFactor: the initial load factor(指定的一个拓展因素,但是因为一般3/4就是最高性能,所以这个参数没有给予实现).
调用了下面的一个参数.
一个参数构造方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public HashMap(int capacity) { if (capacity < 0) { throw new IllegalArgumentException("Capacity: " + capacity); } if (capacity == 0) { @SuppressWarnings("unchecked") HashMapEntry<K, V>[] tab = (HashMapEntry<K, V>[]) EMPTY_TABLE; table = tab; threshold = -1; // Forces first put() to replace EMPTY_TABLE return; } if (capacity < MINIMUM_CAPACITY) { capacity = MINIMUM_CAPACITY; } else if (capacity > MAXIMUM_CAPACITY) { capacity = MAXIMUM_CAPACITY; } else { capacity = Collections.roundUpToPowerOfTwo(capacity); } makeTable(capacity); } |
参数
o capacity:the initial capacity of this hash map(指定的一个初始化容量).
所以一般就会运行这句capacity = Collections.roundUpToPowerOfTwo(capacity);
这句冲洗给capacity
赋值,具体方法是这样的
1 2 3 4 5 6 7 8 9 10 11 12 | public static int roundUpToPowerOfTwo(int i) { i--; // If input is a power of two, shift its high-order bit right. // "Smear" the high-order bit all the way to the right. i |= i >>> 1; i |= i >>> 2; i |= i >>> 4; i |= i >>> 8; i |= i >>> 16; return i + 1; } |
将容量编程了4的倍数且大于原容量.
5->8 6->8 7->8 8->8 9->16 17->32
其实目的就是要让容量能大于等于指定的初始容量,但这个值还得是2的指数倍,即底层数组的长度总是为2的n次方。
根据容量执行了makeTable()方法
1 2 3 4 5 6 7 | private HashMapEntry<K, V>[] makeTable(int newCapacity) { @SuppressWarnings("unchecked") HashMapEntry<K, V>[] newTable = (HashMapEntry<K, V>[]) new HashMapEntry[newCapacity]; table = newTable; threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity return newTable; } |
按照容量初始化了entry表,并且给threshold
重新赋值.
put
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 | @Override public V put(K key, V value) { if (key == null) { return putValueForNullKey(value); } int hash = Collections.secondaryHash(key); HashMapEntry<K, V>[] tab = table; int index = hash & (tab.length - 1); for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) { if (e.hash == hash && key.equals(e.key)) { preModify(e); V oldValue = e.value; e.value = value; return oldValue; } } // No entry for (non-null) key is present; create one modCount++; if (size++ > threshold) { tab = doubleCapacity(); index = hash & (tab.length - 1); } addNewEntry(key, value, hash, index); return null; } void addNewEntry(K key, V value, int hash, int index) { table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]); } |
来看看put方法做了什么:
key==null
首先判断key是否为空,key为空其实就是没有元素,调用putValueForNullKey
方法插入.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | private V putValueForNullKey(V value) { HashMapEntry<K, V> entry = entryForNullKey; if (entry == null) { addNewEntryForNullKey(value); size++; modCount++; return null; } else { preModify(entry); V oldValue = entry.value; entry.value = value; return oldValue; } } |
如果插入以前没有null作为key的情况,就插入一个null key的entry,否则替换原来的null key的value,并且返回老的entrty.
key!=null
o 根据我们插入的entry.key
计算hash
1 | int hash = Collections.secondaryHash(key); |
o 根据hash和tab.length
得到index
1 2 | HashMapEntry<K, V>[] tab = table; int index = hash & (tab.length - 1); |
o 遍历tab[index]
看是否存在 e.hash==hash
且 key.equals(e.key)
o 如果存在就替换旧的值,返回旧的值
o 否则就要准备插入entry了,这时第一件事是判断size++有没有大于阈值,如果大于了就扩容,并且重新hash(为了使entry尽可能分散).
1 2 3 4 | if (size++ > threshold) { tab = doubleCapacity(); index = hash & (tab.length - 1); } |
o 最后调用addNewEntry插入一个新的entry
1 2 3 | void addNewEntry(K key, V value, int hash, int index) { table[index] = new HashMapEntry<K, V>(key, value, hash, table[index]); } |
这样完成了一个entry的插入.
最主要的疑问是:hash在这其中是如何作用的?
hash作用方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | int hash = Collections.secondaryHash(key); public static int secondaryHash(Object key) { return secondaryHash(key.hashCode()); } private static int secondaryHash(int h) { // Spread bits to regularize both segment and index locations, // using variant of single-word Wang/Jenkins hash. h += (h << 15) ^ 0xffffcd7d; h ^= (h >>> 10); h += (h << 3); h ^= (h >>> 6); h += (h << 2) + (h << 14); return h ^ (h >>> 16); } |
这样一个调用链计算对应key的hash,刚刚看到也是一脸懵逼…
可以来说一说HashMap的实现原理了:
HashMap实现原理是数组加链表实现,先根据key的hash来确定其在数组中的位置,如果在哪个位置没有别的元素,那自然最好,直接插入到那个位置.如果那个位置有元素,就将元素插入到那个链表里.
所以key的hash值很重要,它直接决定了entry在hashMap中的位置,不进行hash,直接求index往往都比较集中,entry很容易重复,与我们追求的分散背道而驰,导致我们总是需要遍历很长的链表来查找entry.所以需要使用hash算法把entry分散,让我们在数组里尽可能的不需要遍历链表就能查找到指定的元素,这样的效率最高.
引用一段计算 From Qisen Tang
这里举一个例子,如果现在Table的容量是16,如果最后的结果是 hash & (16-1),也就是 hash & (00001111)。现在我们试着对hash值是31(00011111), 63(00111111),95(01011111)进行操作,理论上映射到的index值应该是不尽相同的,然而实际的情况确实如下的情形:
31=00011111 ==& 00001111==> 1111=15
63=00111111 ==& 00001111==> 1111=15
95=01011111 ==& 00001111==> 1111=15
因此Collections.secondaryHash需要解决的问题,就是避免上面的情况,效果见下面的例子:
31=00011111 ==secondaryHash==> 00011110==& 00001110==> 14
63=00111111 ==secondaryHash==> 00111100==& 00001100==> 12
95=01011111 ==secondaryHash==> 01011010==& 00001010==> 10
1 | int index = hash & (tab.length - 1); |
前面说了,数组长度总是2的n次方.
这时:hash&(length-1)运算等价于对length取模,也就是hash%length,但是&比%具有更高的效率.
通过上面的计算就得到了index,再获取数组tab[index]位置的链表,遍历链表,如果有相同entry则替换(hash相同且key相同).否则插入一个新的元素.
这样完成了put操作.
关于扩容 doubleCapacity
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 | /** * Doubles the capacity of the hash table. Existing entries are placed in * the correct bucket on the enlarged table. If the current capacity is, * MAXIMUM_CAPACITY, this method is a no-op. Returns the table, which * will be new unless we were already at MAXIMUM_CAPACITY. */ private HashMapEntry<K, V>[] doubleCapacity() { HashMapEntry<K, V>[] oldTable = table; //获取旧表长度 int oldCapacity = oldTable.length; //如果旧表已经最大了 没法扩展了 直接返回 if (oldCapacity == MAXIMUM_CAPACITY) { return oldTable; } //新的容量变为旧的两倍 int newCapacity = oldCapacity * 2; //使用makeTable创建新的table HashMapEntry<K, V>[] newTable = makeTable(newCapacity); //size==0 就没有entry了 不用进行其他操作了 可以直接返回表 if (size == 0) { return newTable; } for (int j = 0; j < oldCapacity; j++) { HashMapEntry<K, V> e = oldTable[j]; if (e == null) { continue; } //oldCapacity是2的次方 所以一定是 00010000 的类型 hash与它 & 以后可以得到hash的高位 int highBit = e.hash & oldCapacity; HashMapEntry<K, V> broken = null; //新的table的 index = j|highBit 此时highBit只有高位 j与highBit相或 得到新的index值 newTable[j | highBit] = e; for (HashMapEntry<K, V> n = e.next; n != null; e = n, n = n.next) { int nextHighBit = n.hash & oldCapacity; if (nextHighBit != highBit) { if (broken == null) newTable[j | nextHighBit] = n; else broken.next = n; broken = e; highBit = nextHighBit; } } if (broken != null) broken.next = null; } return newTable; } |
获取旧的容量,旧表容量如果不大于MAXIMUM_CAPACITY,新表容量为旧表容量的两倍,然后创建新表,再将旧表的数据转移到新表中去,在这个过程中会重新计算hash.
在代码中已经写了注释,得到新的index以后就可以遍历将之前index的链表转移到新的table的index位置去.
共同学习,写下你的评论
评论加载中...
作者其他优质文章