java.util.HashMap<K, V>
public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
数组+链表
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;
成员变量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final Entry<?,?>[] EMPTY_TABLE = {};
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; 哈希表,调用构造器创建HashMap之后,哈希表(数组)是一个空表,只有当真正存放元素时,才会创建Entry[]
transient int size; 元素个数,获取当前HashMap的元素个数时,直接返回该成员变量的值,不用遍历整个哈希表
int threshold; 阈值,当哈希表是空时,值为初始容量;当哈希表不为空时,值为容量*加载因子和Integer.MAX_VALUE中较小的一个
final float loadFactor; 加载因子,当元素的个数大于等于哈希表的容量*加载因子,并且当前桶不为空时,扩容
transient int modCount; 记录当前HashMap结构被改变的次数,如改变了HashMap中元素的个数,或者改变了HashMap的内部结构,用于保证并发访问时,若HashMap内部结构发生变化,快速响应失败(ConcurrentModificationException)
transient int hashSeed = 0; 一个随机数,为了使得哈希冲突减少而存在,和真正的哈希值进行异或
构造器
四个重载的,除了public HashMap(Map<? extends K, ? extends V> m)构造器,其它最终调用的都是public HashMap(int initialCapacity, float loadFactor),在构造器中,检查两个参数是否合法,然后初始化成员变量loadFactor(加载因子)和threshold(阈值)。在构造器中,并不真正创建哈希表。只有在第一次put元素时,才真正创建Entry[]哈希表。
this.loadFactor = loadFactor;
threshold = initialCapacity;
loadFactor:加载因子,一般为0.75
threshold:阈值,当哈希表为空时(新建HashMap之后,哈希表是空的,在构造器中将其设为initialCapacity),值为initialCapacity;当哈希表不为空时,值为一般为 容量*加载因子
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的大小和是否为空
HashMap有一个int类型的成员变量size,在每次插入或者删除键值对之后,都会更新size,表示HashMap中元素的个数(Entry的个数),size()方法直接返回size成员变量的值,不需要遍历整个哈希表
判断一个HashMap是否为空,只需要判断size成员变量的值是否恒等于0即可
扩容(resize)
初始大小、容量、加载因子
创建时,未指定初始容量,默认初始大小1<<4(16);创建时,未指定加载因子,默认初始值0.75F
当哈希表中存储的Entry的个数大于等于 容量*加载因子时,扩容。扩容为原来大小的2倍 resize(2 * table.length);
如何扩容?
如果扩容之前的哈希表的大小已经是最大了(没有可扩容的余地了),将阈值threshold设为Integer.MAX_VALUE即可。
如果还可扩容,新建一个大小为原大小两倍的Entry类型的数组,然后调用transfer方法将之间哈希表中的所有Entry重新计算哈希值,放到新建哈希表的相应位置,然后将新建哈希表赋值给成员变量table,并将threshold(阈值)设为容量*加载因子
何时扩容?
(size >= threshold) && (null != table[bucketIndex])
每次向HashMap中put元素,确定好元素要插入的桶的下标之后,如果当前元素的个数大于等于threshold(阈值)并且当前桶中已有元素(有冲突),扩容
为何容量必须为2的n次方?
计算桶的位置(数组下标)时,用hash&(table.length-1),位运算效率高
length-1 用二进制表示,低位始终为全1,这样,扩容之后重新计算下标,只需要看最左边那一位,如果是0下标不变,如果是1下标为原下标+原哈希表长度
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; 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; } } }
存放元素(put)
public V put(K key, V value) 返回值:原value。有原值(存在冲突),返回原value值;否则,返回null
向HashMap中存放元素时,先判断哈希表(table)是不是空表,如果是,调用inflateTable方法构造哈希表
如果是null key,null key保存在table[0]中,遍历table[0],看是否已经存在null key,不存在,新建Entry并插入,存在,新值覆盖旧值
如果不是null key,计算hash(key),根据哈希值得到数组下标indexFor(hash, table.length);
遍历table[index],看是否已经存在该key,不存在,新建Entry并插入,存在,新值覆盖旧值
如何新建Entry并插入?
createEntry方法,调用静态内部类Entry的构造器,将新节点插在链表头部
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); // 一开始,threshold的大小是初始容量(有可能是默认的16,也有可能是构造器中指定的)
private void inflateTable(int toSize) { // Find a power of 2 >= toSize int capacity = roundUpToPowerOf2(toSize);//该方法的作用:如果容量不是2的幂,将容量设置为大于指定值的最小的2的幂的数值 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; initHashSeedAsNeeded(capacity); }
} if (key == null) return putForNullKey(value);//null key的处理
private V putForNullKey(V value) {//HashMap将Null key存储在Entry[]下标为0的链表中 for (Entry<K,V> e = table[0]; e != null; e = e.next) {//遍历Entry[]数组下标为0的链表 if (e.key == null) {//null key只能存在一个,如果发现已经有null key了,用新value覆盖旧value V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0);//不存在null key新建Entry并添加 return null; }
//不是 null key
int hash = hash(key); // 计算key的哈希值
final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); //存储在HashMap中的hash值并不是key对象的hashCode,而是其hashCode经过一系列异或位移运算之后的结果,目的是减少冲突 h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
int i = indexFor(hash, table.length); // 计算当前键值对应该存储的位置,下标
static int indexFor(int h, int length) { return h & (length-1); //因为哈希表的长度始终是2的幂次方,因此,使用位运算,效率高 }
// 已经确定了要存储的下标,遍历该位置的链表,如果存在同一key,则用新值覆盖旧值
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))) {//如何确定两个key一样?哈希值相等并且equals V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++;
//如果没有一样的key,则新建并插入 addEntry(hash, key, value, i);
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;//扩容之后要重新计算hash值 bucketIndex = indexFor(hash, table.length);//找到扩容之后应该存放的位置(下标) } createEntry(hash, key, value, bucketIndex);//在该方法中,创建Entry,插入到当前链表的头部,并将size+1 }
return null; }
获取元素(get)
public V get(Object key) 根据key返回对应的value值
返回null: 两种情况(1)key对应的value为null(2)HashMap中不存在该key值对应的键值对
要判断HashMap中是否存在某个key,应使用containsKey方法
public V get(Object key) { if (key == null) return getForNullKey();//null key
null key
(1)如果size==0,返回null(不存在该key对应的键值对)
(2)遍历table[0],null key存储在table[0]中
如果有恒等于null的键,则返回该键对应的null
否则,返回null
非null key
(1)size是否为0
(2)计算key对应的hash值(调用hash(key)方法),根据hash值得到下标(调用indexFor(hash,table.length))
遍历table[index]
如果在当前桶中存在一个Entry,其hash值恒等于要查找的key的hash(key),并且key相等(== equals),表明存在要查找的key,返回对应的Entry对象
否则,返回null
Entry<K,V> entry = getEntry(key);// key != null return null == entry ? null : entry.getValue(); }
判断是否包含某个key
public boolean containsKey(Object key) { return getEntry(key) != null; }
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; }
判断是否包含某个value
public boolean containsValue(Object value) { if (value == null) return containsNullValue(); Entry[] tab = table; for (int i = 0; i < tab.length ; i++) for (Entry e = tab[i] ; e != null ; e = e.next) if (value.equals(e.value)) return true; return false; }
删除某个key对应的键值对
public V remove(Object key)
如果key存在,返回key对应的value值;如果key不存在,返回null
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; }