结构
数组+链表
- 数组:key-value形式的Entry数组
- 链地址法解决哈希冲突,相同桶位的元素咦链表形式连接存储

// 表,根据需要调整大小。长度必须是2的幂。transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; // 数组static class Entry<K,V> implements Map.Entry<K,V> {final K key;V value;Entry<K,V> next; // 链表int hash;}
创建
// 构造一个具有指定初始容量和加载因子的空HashMappublic 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,具有指定的初始容量和默认加载因子(0.75)public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}// 构造一个具有默认初始容量(16)和默认负载系数(0.75)的空HashMappublic HashMap() {this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);}/*** 使用与指定Map相同的映射构造一个新的HashMap* HashMap使用默认的装载因子(0.75)创建,初始容量足以容纳指定映射中的映射*/public HashMap(Map<? extends K, ? extends V> m) {// 创建一个指定容量,默认负载因子的HashMapthis(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);// 初始化Entry[]数组inflateTable(threshold);// 将m中的元素转移到HashMap中putAllForCreate(m);}
默认创建一个容量是16、负载因子是0.75的HashMap,初始Entry[]数组为空{},在第一次进行put操作插入元素时完成Entry[]的初始化
put操作
/*** 将指定的值与此映射中的指定键关联* 如果该映射先前包含了该键的映射,则旧值将被替换*/public V put(K key, V value) {if (table == EMPTY_TABLE) {// 初始化Entry[]数组inflateTable(threshold);}// 添加null键的entryif (key == null)return putForNullKey(value);// 计算key的哈希值,hash()方法保证key的hashcode计算出来后散列性更强int hash = hash(key);// 计算出在entry[]的索引位置int i = indexFor(hash, table.length);// 遍历entry[i]位置的链表,判断是否已经存在该key,存在即覆盖for (Entry<K,V> e = table[i]; e != null; e = e.next) {Object k;// 覆盖相同key的entry的value值,并返回旧的entry的value值if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue;}}// 记录修改次数+1modCount++;// 添加元素到map中addEntry(hash, key, value, i);return null;}
putForNullKey
支持一个null键,多个null值
private V putForNullKey(V value) {// 使用Entry[]的0号位置存放null键的entryfor (Entry<K,V> e = table[0]; e != null; e = e.next) {// HashMap中已经存在null键entryif (e.key == null) {// 覆盖null键的entry值V oldValue = e.value;e.value = value;// 每当对HashMap中已经存在的键k调用put(k,v)覆盖条目中的值时,就会调用这个方法e.recordAccess(this);return oldValue;}}// 修改次数+1,用于多线程环境下数据被其他修改后快速抛出异常,fail-fastmodCount++;// 添加元素到map中addEntry(0, null, value, 0);return null;}
addEntry
// 添加元素到map中void addEntry(int hash, K key, V value, int bucketIndex) {// 判断是否需要扩容if ((size >= threshold) && (null != table[bucketIndex])) {// 扩容,容量*2resize(2 * table.length);hash = (null != key) ? hash(key) : 0;// 计算key应该存储在新entry[]的桶位bucketIndex = indexFor(hash, table.length);}// 添加entry到集合中createEntry(hash, key, value, bucketIndex);}
createEntry
void createEntry(int hash, K key, V value, int bucketIndex) {// 该桶位头节点Entry<K,V> e = table[bucketIndex];// 插入该桶位中,next指针指向之前的头节点table[bucketIndex] = new Entry<>(hash, key, value, e);// 记录集合中元素个数加1size++;}
扩容操作
- Entry[]容量扩大2倍
- 将旧Entry[]元素移动到新的Entry[]数组中(是否rehash)
resize
该方法主要用于扩容
/*** 将此map的内容重新散列到一个具有更大容量的新数组中* 当此映射中的键数达到其阈值时,将自动调用此方法* 如果当前容量为MAXIMUM_CAPACITY,该方法不会调整映射的大小,而是将阈值设置Integer.MAX_VALUE*/void resize(int newCapacity) {// 存放旧的entry[]数值Entry[] oldTable = table;// 存放旧的entry[]数组容量int oldCapacity = oldTable.length;// 旧的数组容量 == 最大容量if (oldCapacity == MAXIMUM_CAPACITY) {// 扩容阈值设置为最大整数threshold = Integer.MAX_VALUE;return;}// 创建一个容量为newCapacity的新entry[]数组Entry[] newTable = new Entry[newCapacity];// 将旧entry[]数组中的元素迁移到新的entry[]数组中transfer(newTable, initHashSeedAsNeeded(newCapacity));// 设置HashMap的table变量指向新的entry[]数组table = newTable;// 重新设置扩容阈值threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);}
transfer
该方法主要用于扩容时把旧entry数组中的全部元素迁移迁移到新的entry数组中
void transfer(Entry[] newTable, boolean rehash) {// 新entry[]数组的容量int newCapacity = newTable.length;// entry数组的每个桶位for (Entry<K,V> e : table) {// 每个桶位中的链表遍历while(null != e) {// 链表的下一个entry节点Entry<K,V> next = e.next;// rehash绝大多数情况下都是falseif (rehash) {e.hash = null == e.key ? 0 : hash(e.key);}// 计算出entry节点在新的entry[]数组中的索引int i = indexFor(e.hash, newCapacity);// 头插法将entry节点插入新entry[]数组中(这是多线程情景下导致成环的主要原因)e.next = newTable[i];newTable[i] = e;// 遍历下一个节点e = next;}}}
扩容一般是扩大2倍,这样的好处是:
容量n为2的幂次方,n-1的二进制会全为1,位运算时可以充分散列,避免不必要的哈希冲突。所以扩容必须2倍就是为了维持容量始终为2的幂次方
原先容量为a,现在容量为2a,那么原先索引为x,现在索引为x或者(x+a)
get操作
// 返回指定键映射到的值,如果该map不包含该键的映射,则返回nullpublic V get(Object key) {// key为null的查找if (key == null)return getForNullKey();// 根据key查找对应的entry节点Entry<K,V> entry = getEntry(key);// 从entry节点中获取对应的value值return null == entry ? null : entry.getValue();}
getForNullKey
/*** 专门查找空键的方法。Null键映射到索引0** 为了在两种最常用的操作(get和put)中提高性能,这个空情况被拆分为单独的方法,* 但在其他操作中与条件合并*/private V getForNullKey() {// map中没有元素,直接返回nullif (size == 0) {return null;}// table数组0号位置冲突链上查找key为null的entry对应的value值for (Entry<K,V> e = table[0]; e != null; e = e.next) {if (e.key == null)return e.value;}return null;}
getEntry
// 根据key在HashMap中查找相关联的entryfinal Entry<K,V> getEntry(Object key) {// map中没有元素,直接返回nullif (size == 0) {return null;}// 计算key的哈希值int hash = (key == null) ? 0 : hash(key);// 找到table索引位置,遍历该索引位置的冲突链for (Entry<K,V> e = table[indexFor(hash, table.length)];e != null;e = e.next) {Object k;// 找到对应的key的entry,返回这个entry节点if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;}return null;}
