HashMap存储的是键值对,在HashMap内部用一个Entry表示键值对:
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; // 该键值对的下一个键值对的指针 Entry<K,V> next; // key的哈希值 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(); } /** * 当一个已经存在的键值对的值被put(k,v)方法修改时,这个方法被调用 */ void recordAccess(HashMap<K,V> m) { } /** * 这个方法在这个键值对从哈希表中删除调用 */ void recordRemoval(HashMap<K,V> m) { } }从Entry可以看出,HashMap是通过链接法来解决碰撞的,而且桶中的链表是单链表。存储键值对的数组定义如下:
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;默认情况下,table初始化为空表:
static final Entry<?,?>[] EMPTY_TABLE = {};
HashMap有一个值保存着其中键值对的数量:
transient int size;
int threshold;
final float loadFactor;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
还有一个最大容量:
static final int MAXIMUM_CAPACITY = 1 << 30;HashMap的容量必须是2整数次幂。
接下来看看HashMap里的操作。
(1)向HashMap中添加一个键值对
public V put(K key, V value) { // 如果table之前为空,就创建一个更大的table if (table == EMPTY_TABLE) { inflateTable(threshold); } // 如果key为null,就用对应的put方法 if (key == null) return putForNullKey(value); // 计算key的哈希值 int hash = hash(key); // 根据哈希值和table长度计算该键值对应该存储在那个桶中 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; }
private V putForNullKey(V value) { /** * 键为null的键值对的键的哈希值为0,存储在table[0]中; * for循环查看table[0]中是否有key为null的键值对,如果 * 存在,就把该键值对得值改为value */ for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; // 如果table[0]中没有键为null的键值对,就添加一个键为null,值为value的键值对到table[0]中 addEntry(0, null, value, 0); return null; }
void addEntry(int hash, K key, V value, int bucketIndex) { /** * 如果size大于等于threshold,并且指定的放键值对的桶中已经有元素, * 就创建一个更大的table */ if ((size >= threshold) && (null != table[bucketIndex])) { // 创建一个新的大的数组来存储键值对 resize(2 * table.length); // 重新计算key的哈希值 hash = (null != key) ? hash(key) : 0; // 计算键值对应该放在哪个桶里 bucketIndex = indexFor(hash, table.length); } // 实际上调用该方法把键值对放进指定桶中的 createEntry(hash, key, value, bucketIndex); }resize(int newCapacity)的实现如下:
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; /** * 如果旧table的容量已经等于最大容量,就不再扩大table, * 只是增加threshold的值 */ if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } // 根据指定的新的容量,创建一个更大的数组 Entry[] newTable = new Entry[newCapacity]; // 把旧数组中的键值对全部复制到新数组中 transfer(newTable, initHashSeedAsNeeded(newCapacity)); // 把新数组赋给table table = newTable; // 设置threshold threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
// 把table里的元素全部放进newTable中 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; } } }
void createEntry(int hash, K key, V value, int bucketIndex) { // 用e保存table[bucketIndex] Entry<K,V> e = table[bucketIndex]; // 以hash,key,value,e创建一个键值对,e作为这个键值对的下一个元素 table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }把一个键值对放进HashMap的过程就是找到一个桶,然后把键值对放进这个桶中的过程,因为只需要在桶中的单链表中添加元素,这个过程很快。
(2)通过key取出键值对得value
public V get(Object key) { //如果key为null,调用相应方法取回value if (key == null) return getForNullKey(); // 如果key不为null,调用getEntry(Object key)返回key对应的Entry Entry<K,V> entry = getEntry(key); // 如果entry为null,就返回null,否则返回 entry的value return null == entry ? null : entry.getValue(); }
getForNullKey()的实现如下:
private V getForNullKey() { // size等于0,说明table中没有键值对 if (size == 0) { return null; } // 键为null的键值对存放在table[0]中,table[0],找到键值对,返回value for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } // 如果HashMap中没有包含这个key为null的键值对,就返回null return null; }
final Entry<K,V> getEntry(Object key) { // size等于0,说明table中没有键值对 if (size == 0) { return null; } // 计算哈希值 int hash = (key == null) ? 0 : hash(key); // 计算出该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; } // 如果HashMap中没有包含键为key的键值对,就返回null return null; }HashMap只是从某一个桶中查找键值对,可见,查找速度很快。
public V remove(Object key) { // 删除键位key的键值对,并返回对应建筑对 Entry<K,V> e = removeEntryForKey(key); // 如果e为null,就返回null,否则返回e中的值 return (e == null ? null : e.value); }
final Entry<K,V> removeEntryForKey(Object key) { // 如果size等于0,就说明table为空,不包含键为key的键值对,返回null if (size == 0) { return null; } // 计算key的哈希值 int hash = (key == null) ? 0 : hash(key); // 计算键为key的键值对在哪个桶中 int i = indexFor(hash, table.length); // 获得table[i] Entry<K,V> prev = table[i]; Entry<K,V> e = prev; // 遍历单链表 while (e != null) { Entry<K,V> next = e.next; Object k; // 如果找到键为key的键值对,就将其删除 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; /** * 如果e为头节点,只有e为头节点时,它们才相等; * 否则将前驱的指向下一个节点的指针指向后继 */ if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } // 现在的e就是是下一个元素的前驱 prev = e; e = next; } /** * 如果没找到键为key的键值对,e就是null; * 否则就是key对应的键值对 */ return e; }删除一个元素,也是在桶中寻找是否包含这个元素,然后将其删除,可见,从HashMap中删除一个元素也是很快的。
(4)查看HashMap中是否包含某一个键
public boolean containsKey(Object key) { // 如果通过这个key得到的entry不是null,就说明HashMap中包含键为key的键值对 return getEntry(key) != null; }getEntry(Object key)方法在上面已经讲过了。