HashMap原理解析–JDK1.7
今天无意间看Spring Core的源码,里面有一个HashSet,手一滑点进了源码查看,发现HashSet是用HashMap实现的。瞬间想到了当时准备面试时的场景。背了那么多Java Collection的概念,竟然都没有仔细看过任何一个类的源码。今天索性就从HashMap开刀仔细看看这些基本数据结构的实现。
从jdk1.7到1.8,HashMap的数据组织结构经历了比较彻底的转变,这篇文章主要介绍JDK1.7的实现
在JDK1.7中,HashMap的基本构成是通过邻接表的方式实现的,大致结构如下图所示
我们来看一下其中的数据结构
Entry
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } public final K getKey() { return key; } public final V getValue() { return value; } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; Object k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } public final int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); } public final String toString() { return getKey() + "=" + getValue(); } /** * This method is invoked whenever the value in an entry is * overwritten by an invocation of put(k,v) for a key k that's already * in the HashMap. */ void recordAccess(HashMap<K,V> m) { } /** * This method is invoked whenever the entry is * removed from the table. */ void recordRemoval(HashMap<K,V> m) { } }
这个Entry<K,V>实现了Map.Entry这个内部接口,这个类中有下面几个方法
interface Entry<K,V> { K getKey(); V getValue(); V setValue(V value); boolean equals(Object o); int hashCode(); }
相比与这个接口,HashMap中的Entry主要是添加了Entry next使其具有了链表的特性,接着我们看一些关键的方法。
一般我们使用如下方法创建HashMap
Map<String, String> map = new HashMap();
我们直接看HashMap()构造方法
public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }
其中的两个常量DEFAULT_INITIAL_CAPACITY=1 << 4;(16)DEFAULT_LOAD_FACTOR=0.75这个常量的含义我们在后面详细说明。
这个构造方法调用了同名的构造方法
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(); }
1、如果bucket数组为空,调用inflateTable方法完成初始化; 2、判断待插入key是否为null: key == null成立,调用putForNullKey方法完成数据插入,由此可以看出,HashMap的key是可以为null的; key == null不成立,跳转到步骤3; 3、计算带插入key的hashCode; 4、根据hashCode按位与计算出所在bucket数组中的位置i; 5、遍历挂在bucket中位置i下的Entry链表,如果当前key已存在,更新它所对应的oldValue为value,并返回oldValue,否则,跳转到步骤6; 6、将key-value插入对应bucket中位置i下的Entry链表中,返回null。
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; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
我们可以看到,在这个方法中,首先会初始化邻接表的各个Hash桶,通过调用tableinflateTable(threshold)方法
private void inflateTable(int toSize) { // Find a power of 2 >= toSize int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; initHashSeedAsNeeded(capacity); }
其中主要的步骤如下:
private static int roundUpToPowerOf2(int number) { // assert number >= 0 : "number must be non-negative"; return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; }
final boolean initHashSeedAsNeeded(int capacity) { boolean currentAltHashing = hashSeed != 0; boolean useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); boolean switching = currentAltHashing ^ useAltHashing; if (switching) { hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0; } return switching; }
回到put方法,随后将key做hash,然后根据这个hash值找到具体的table的索引值
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); }
对于jdk,用了一个很巧的方法完成了取余的操作。值得学习(^v^)。
当找到所在的Entry数组的下标后,会遍历这个由这个下标所在的Entry为头的链表,在这个链表下链接的都是hash完之后值相同的key,在遍历这个链表时,如果key是相同的,那么直接替换掉这个key对应的value,将原先的value返回。如果没有,则会创建新的Entry,然后头插入这个链表。那么我们就来看看添加新Entry的过程:
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); }
这个方法中有一个比较重要的判断条件,就是是否需要扩容,如果当前的size已经达到阈值并且这个表格最后一个下标已经没有空的链表位置,那么就需要扩容,这里的扩容大小为原来大小的两倍,resize函数中其实和我们最早大学学习数据结构时教材上的执行没有两样,代码如下,总体思路如下:创建一个两倍大小的table,并将原来的table放在了新的table中,同时设置一些属性信息,对于其中有一个loadFactor,这里的目的是为了能够尽可能减少冲突的概率,增加查找效率,随后通过initHashSeedAsNeeded重新设置hashseed,最后创建新的Entry。
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); }
这里面有一个特殊的case,就是key为null的情况,这样的情况会直接连接到Entry[0]的链表中,通过插入的逻辑可以知道只能插入一个key为空的value,如果多个,只有最后一个有效。
我们来看get方法,直观来想,是put方法的逆过程,即:
1、拿到key的hash值 2、找到这个hash对应的Entry 3、遍历这个Entry为头对应的链表,直到找到对应的元素 4、如果没找到,返回null
具体的代码如下:
public V get(Object key) { if (key == null) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); }
我们看基本和我们上面的预期基本相同,这里面有个getEntry方法,其思路就是根据key的hash找下标遍历链表的过程,代码如下,具体的和put方法类似,这里不再赘述。
final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }
还有一个方法,containsKey也是基于get方法实现,有兴趣的大家可以自行看代码。
这个方法也和get,put方法类似,不同的就是删除Entry的时候,这个就是链表中删除特定节点的操作,具体代码如下:
public V remove(Object key) { Entry<K,V> e = removeEntryForKey(key); return (e == null ? null : e.value); }
final Entry<K,V> removeEntryForKey(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; while (e != null) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; }
最后,我们就要看Map的遍历了,常见的Map遍历有如下几种
1、获取keySet,迭代key获取value 2、获取entrySet,直接迭代,获取key和value
我们简单看一下其实现:
public Set<K> keySet() { Set<K> ks = keySet; return (ks != null ? ks : (keySet = new KeySet())); } private final class KeySet extends AbstractSet<K> { public Iterator<K> iterator() { return newKeyIterator(); } public int size() { return size; } public boolean contains(Object o) { return containsKey(o); } public boolean remove(Object o) { return HashMap.this.removeEntryForKey(o) != null; } public void clear() { HashMap.this.clear(); } }
public Set<Map.Entry<K,V>> entrySet() { return entrySet0(); } private Set<Map.Entry<K,V>> entrySet0() { Set<Map.Entry<K,V>> es = entrySet; return es != null ? es : (entrySet = new EntrySet()); } private final class EntrySet extends AbstractSet<Map.Entry<K,V>> { public Iterator<Map.Entry<K,V>> iterator() { return newEntryIterator(); } public boolean contains(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<K,V> e = (Map.Entry<K,V>) o; Entry<K,V> candidate = getEntry(e.getKey()); return candidate != null && candidate.equals(e); } public boolean remove(Object o) { return removeMapping(o) != null; } public int size() { return size; } public void clear() { HashMap.this.clear(); } }
这里面都是用了一个相同的实现机制,就是通过内部类调用该内部类所在的外部对象,这样实现的目的是为了,当外部对象发生变化时,这个内部类对象会发生变化,同时,内部类对象改变时也会改变外部对象的结果,而如果使用副本时,外部对象的修改对内部类对象没有影响,返回的值不确定。
这就是JDK1.7中HashMap的实现,下篇文章中,我将介绍JDK1.8中的相关变更。
谢谢你请我吃糖果