类定义java
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
主要介绍数组
HashMap 采用的数据结构 = 数组(主) + 单链表(副) ,具体描述以下安全
该数据结构方式也称:拉链法数据结构
注:为了让你们有个感性的认识,只是简单的画出存储流程,更加详细 & 具体的存储流程会在下面源码分析中给出多线程
HashMap 中的数组元素 & 链表节点 采用 Entry 类 实现,以下图所示并发
该类的源码分析以下函数
具体分析请看注释oop
/** * Entry类实现了Map.Entry接口 * 即 实现了getKey()、getValue()、equals(Object o)和hashCode()等方法 **/ static class Entry<K,V> implements Map.Entry<K,V> { final K key; // 键 V value; // 值 Entry<K,V> next; // 指向下一个节点 ,也是一个Entry对象,从而造成解决hash冲突的单链表 int hash; // hash值 /** * 构造方法,建立一个Entry * 参数:哈希值h,键值k,值v、下一个节点n */ 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; } /** * equals() * 做用:判断2个Entry是否相等,必须key和value都相等,才返回true */ 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; } /** * hashCode() */ public final int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); } public final String toString() { return getKey() + "=" + getValue(); } /** * 当向HashMap中添加元素时,即调用put(k,v)时, * 对已经在HashMap中k位置进行v的覆盖时,会调用此方法 * 此处没作任何处理 */ void recordAccess(HashMap<K,V> m) { } /** * 当从HashMap中删除了一个Entry时,会调用该函数 * 此处没作任何处理 */ void recordRemoval(HashMap<K,V> m) { } }
V get(Object key); // 得到指定键的值 V put(K key, V value); // 添加键值对 void putAll(Map<? extends K, ? extends V> m); // 将指定Map中的键值对 复制到 此Map中 V remove(Object key); // 删除该键值对 boolean containsKey(Object key); // 判断是否存在该键的键值对;是 则返回true boolean containsValue(Object value); // 判断是否存在该值的键值对;是 则返回true Set<K> keySet(); // 单独抽取key序列,将全部key生成一个Set Collection<V> values(); // 单独value序列,将全部value生成一个Collection void clear(); // 清除哈希表中的全部键值对 int size(); // 返回哈希表中全部 键值对的数量 = 数组中的键值对 + 链表中的键值对 boolean isEmpty(); // 判断HashMap是否为空;size == 0时 表示为 空
在具体使用时,主要流程是:源码分析
示例代码优化
import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; public class HashMapTest { public static void main(String[] args) { /** * 1. 声明1个 HashMap的对象 */ Map<String, Integer> map = new HashMap<String, Integer>(); /** * 2. 向HashMap添加数据(成对 放入 键 - 值对) */ map.put("Android", 1); map.put("Java", 2); map.put("iOS", 3); map.put("数据挖掘", 4); map.put("产品经理", 5); /** * 3. 获取 HashMap 的某个数据 */ System.out.println("key = 产品经理时的值为:" + map.get("产品经理")); /** * 4. 获取 HashMap 的所有数据:遍历HashMap * 核心思想: * 步骤1:得到key-value对(Entry) 或 key 或 value的Set集合 * 步骤2:遍历上述Set集合(使用for循环 、 迭代器(Iterator)都可) * 方法共有3种:分别针对 key-value对(Entry) 或 key 或 value */ // 方法1:得到key-value的Set集合 再遍历 System.out.println("方法1"); // 1. 得到key-value对(Entry)的Set集合 Set<Map.Entry<String, Integer>> entrySet = map.entrySet(); // 2. 遍历Set集合,从而获取key-value // 2.1 经过for循环 for(Map.Entry<String, Integer> entry : entrySet){ System.out.print(entry.getKey()); System.out.println(entry.getValue()); } System.out.println("----------"); // 2.2 经过迭代器:先得到key-value对(Entry)的Iterator,再循环遍历 Iterator iter1 = entrySet.iterator(); while (iter1.hasNext()) { // 遍历时,需先获取entry,再分别获取key、value Map.Entry entry = (Map.Entry) iter1.next(); System.out.print((String) entry.getKey()); System.out.println((Integer) entry.getValue()); } // 方法2:得到key的Set集合 再遍历 System.out.println("方法2"); // 1. 得到key的Set集合 Set<String> keySet = map.keySet(); // 2. 遍历Set集合,从而获取key,再获取value // 2.1 经过for循环 for(String key : keySet){ System.out.print(key); System.out.println(map.get(key)); } System.out.println("----------"); // 2.2 经过迭代器:先得到key的Iterator,再循环遍历 Iterator iter2 = keySet.iterator(); String key = null; while (iter2.hasNext()) { key = (String)iter2.next(); System.out.print(key); System.out.println(map.get(key)); } // 方法3:得到value的Set集合 再遍历 System.out.println("方法3"); // 1. 得到value的Set集合 Collection valueSet = map.values(); // 2. 遍历Set集合,从而获取value // 2.1 得到values 的Iterator Iterator iter3 = valueSet.iterator(); // 2.2 经过遍历,直接获取value while (iter3.hasNext()) { System.out.println(iter3.next()); } } } // 注:对于遍历方式,推荐使用针对 key-value对(Entry)的方式:效率高 // 缘由: // 1. 对于 遍历keySet 、valueSet,实质上 = 遍历了2次:1 = 转为 iterator 迭代器遍历、2 = 从 HashMap 中取出 key 的 value 操做(经过 key 值 hashCode 和 equals 索引) // 2. 对于 遍历 entrySet ,实质 = 遍历了1次 = 获取存储实体Entry(存储了key 和 value )
下面,咱们按照上述的使用过程,对一个个步骤进行源码解析
// 1. 容量(capacity): HashMap中数组的长度 // a. 容量范围:必须是2的幂 & <最大容量(2的30次方) // b. 初始容量 = 哈希表建立时的容量 // 默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 10000 = 十进制的2^4=16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 最大容量 = 2的30次方(若传入的容量过大,将被最大值替换) static final int MAXIMUM_CAPACITY = 1 << 30; // 2. 加载因子(Load factor):HashMap在其容量自动增长前可达到多满的一种尺度 // a. 加载因子越大、填满的元素越多 = 空间利用率高、但冲突的机会加大、查找效率变低(由于链表变长了) // b. 加载因子越小、填满的元素越少 = 空间利用率小、冲突的机会减少、查找效率高(链表不长) // 实际加载因子 final float loadFactor; // 默认加载因子 = 0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 3. 扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量) // a. 扩容 = 对哈希表进行resize操做(即重建内部数据结构),从而哈希表将具备大约两倍的桶数 // b. 扩容阈值 = 容量 x 加载因子 int threshold; // 4. 其余 // 存储数据的Entry类型 数组,长度 = 2的幂 // HashMap的实现方式 = 拉链法,Entry数组上的每一个元素本质上是一个单向链表 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; // HashMap的大小,即 HashMap中存储的键值对的数量 transient int size;
注:
至此,关于 HashMap 的构造函数讲解完毕。
添加数据的流程以下
注:为了让你们有个感性的认识,只是简单的画出存储流程,更加详细 & 具体的存储流程会在下面源码分析中给出
/** * 函数使用原型 */ map.put("Android", 1); map.put("Java", 2); map.put("iOS", 3); map.put("数据挖掘", 4); map.put("产品经理", 5); /** * 源码分析:主要分析: HashMap的put函数 */ public V put(K key, V value) (分析1)// 1. 若 哈希表未初始化(即 table为空) // 则使用 构造函数时设置的阈值(即初始容量) 初始化 数组table if (table == EMPTY_TABLE) { inflateTable(threshold); } // 2. 判断key是否为空值null (分析2)// 2.1 若key == null,则将该键-值 存放到数组table 中的第1个位置,即table [0] // (本质:key = Null时,hash值 = 0,故存放到table[0]中) // 该位置永远只有1个value,新传进来的value会覆盖旧的value if (key == null) return putForNullKey(value); (分析3) // 2.2 若 key ≠ null,则计算存放数组 table 中的位置(下标、索引) // a. 根据键值key计算hash值 int hash = hash(key); // b. 根据hash值 最终得到 key对应存放的数组Table中位置 int i = indexFor(hash, table.length); // 3. 判断该key对应的值是否已存在(经过遍历 以该数组元素为头结点的链表 逐个判断) for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; (分析4)// 3.1 若该key已存在(即 key-value已存在 ),则用 新value 替换 旧value if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; //并返回旧的value } } modCount++; (分析5)// 3.2 若 该key不存在,则将“key-value”添加到table中 addEntry(hash, key, value, i); return null; }
即 初始化数组( table )、扩容阈值( threshold )
/** * 函数使用原型 */ if (table == EMPTY_TABLE) { inflateTable(threshold); } /** * 源码分析:inflateTable(threshold); */ private void inflateTable(int toSize) { // 1. 将传入的容量大小转化为:>传入容量大小的最小的2的次幂 // 即若是传入的是容量大小是19,那么转化后,初始化容量大小为32(即2的5次幂) int capacity = roundUpToPowerOf2(toSize);->>分析1 // 2. 从新计算阈值 threshold = 容量 * 加载因子 threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); // 3. 使用计算后的初始容量(已是2的次幂) 初始化数组table(做为数组长度) // 即 哈希表的容量大小 = 数组大小(长度) table = new Entry[capacity]; //用该容量初始化table initHashSeedAsNeeded(capacity); } /** * 分析1:roundUpToPowerOf2(toSize) * 做用:将传入的容量大小转化为:>传入容量大小的最小的2的幂 * 特别注意:容量大小必须为2的幂,该缘由在下面的讲解会详细分析 */ private static int roundUpToPowerOf2(int number) { //若 容量超过了最大值,初始化容量设置为最大值 ;不然,设置为:>传入容量大小的最小的2的次幂 return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
分析2:当 key ==null时,将该 key-value 的存储位置规定为数组table 中的第1个位置,即table [0]
/** * 函数使用原型 */ if (key == null) return putForNullKey(value); /** * 源码分析:putForNullKey(value) */ private V putForNullKey(V value) { // 遍历以table[0]为首的链表,寻找是否存在key==null 对应的键值对 // 1. 如有:则用新value 替换 旧value;同时返回旧的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++; // 2 .若无key==null的键,那么调用addEntry(),将空键 & 对应的值封装到Entry中,并放到table[0]中 addEntry(0, null, value, 0); // 注: // a. addEntry()的第1个参数 = hash值 = 传入0 // b. 即 说明:当key = null时,也有hash值 = 0,因此HashMap的key 可为null // c. 对比HashTable,因为HashTable对key直接hashCode(),若key为null时,会抛出异常,因此HashTable的key不可为null // d. 此处只需知道是将 key-value 添加到HashMap中便可,关于addEntry()的源码分析将等到下面再详细说明, return null; }
今后处能够看出:
- HashMap 的键 key 可为 null (区别于 HashTable 的 key 不可为 null )
- HashMap 的键 key 可为 null 且只能为1个,但值 value 可为null且为多个
/** * 函数使用原型 * 主要分为2步:计算hash值、根据hash值再计算得出最后数组位置 */ // a. 根据键值key计算hash值 ->> 分析1 int hash = hash(key); // b. 根据hash值 最终得到 key对应存放的数组Table中位置 ->> 分析2 int i = indexFor(hash, table.length); /** * 源码分析1:hash(key) * 该函数在JDK 1.7 和 1.8 中的实现不一样,但原理同样 = 扰动函数 = 使得根据key生成的哈希码(hash值)分布更加均匀、更具有随机性,避免出现hash值冲突(即指不一样key但生成同1个hash值) * JDK 1.7 作了9次扰动处理 = 4次位运算 + 5次异或运算 * JDK 1.8 简化了扰动函数 = 只作了2次扰动 = 1次位运算 + 1次异或运算 */ // JDK 1.7实现:将 键key 转换成 哈希码(hash值)操做 = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动) static final int hash(int h) { h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } // JDK 1.8实现:将 键key 转换成 哈希码(hash值)操做 = 使用hashCode() + 1次位运算 + 1次异或运算(2次扰动) // 1. 取hashCode值: h = key.hashCode() // 2. 高位参与低位的运算:h ^ (h >>> 16) static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // a. 当key = null时,hash值 = 0,因此HashMap的key 可为null // 注:对比HashTable,HashTable对key直接hashCode(),若key为null时,会抛出异常,因此HashTable的key不可为null // b. 当key ≠ null时,则经过先计算出 key的 hashCode()(记为h),而后 对哈希码进行 扰动处理: 按位 异或(^) 哈希码自身右移16位后的二进制 } /** * 函数源码分析2:indexFor(hash, table.length) * JDK 1.8中实际上无该函数,但原理相同,即具有相似做用的函数 */ static int indexFor(int h, int length) { return h & (length-1); // 将对哈希码扰动处理后的结果 与运算(&) (数组长度-1),最终获得存储在数组table的位置(即数组下标、索引) }
总结 计算存放在数组 table 中的位置(即数组下标、索引)的过程
在了解 如何计算存放数组 table 中的位置 后,所谓 知其然 而 需知其因此然 ,下面我将讲解为何要这样计算,即主要解答如下3个问题:
1. 为何不直接采用通过 hashCode() 处理的哈希码 做为 存储数组 table 的下标位置?
2. 为何采用 哈希码 与运算(&) (数组长度-1) 计算数组下标?
3. 为何在计算数组下标前,需对哈希码进行二次处理:扰动处理?
在回答这3个问题前,请你们记住一个核心思想:
全部处理的根本目的,都是为了提升 存储 key-value 的数组下标位置 的随机性 & 分布均匀性,尽可能避免出现hash值冲突 。即:对于不一样 key ,存储的数组下标位置要尽量不同
至此,关于怎么计算 key-value 值存储在 HashMap 数组位置 & 为何要这么计算,讲解完毕。
注:当发生 Hash 冲突时,为了保证 键 key 的惟一性哈希表并不会立刻在链表中插入新数据,而是先查找该 key 是否已存在,若已存在,则替换便可
/** * 函数使用原型 */ // 2. 判断该key对应的值是否已存在(经过遍历 以该数组元素为头结点的链表 逐个判断) for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; // 2.1 若该key已存在(即 key-value已存在 ),则用 新value 替换 旧value if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; //并返回旧的value } } modCount++; // 2.2 若 该key不存在,则将“key-value”添加到table中 addEntry(hash, key, value, i); return null;
分析1:替换流程
具体以下图:
分析2: key 值的比较
采用 equals() 或 “==” 进行比较,下面给出其介绍 & 与 “==” 使用的对比
/** * 函数使用原型 */ // 2. 判断该key对应的值是否已存在 for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; // 2.1 若该key对应的值已存在,则用新的value取代旧的value if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; // 2.2 若 该key对应的值不存在,则将“key-value”添加到table中 addEntry(hash, key, value, i); /** * 源码分析:addEntry(hash, key, value, i) * 做用:添加键值对(Entry )到 HashMap中 */ void addEntry(int hash, K key, V value, int bucketIndex) { // 参数3 = 插入数组table的索引位置 = 数组下标 // 1. 插入前,先判断容量是否足够 // 1.1 若不足够,则进行扩容(2倍)、从新计算Hash值、从新计算存储数组下标 if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); // a. 扩容2倍 --> 分析1 hash = (null != key) ? hash(key) : 0; // b. 从新计算该Key对应的hash值 bucketIndex = indexFor(hash, table.length); // c. 从新计算该Key对应的hash值的存储数组下标位置 } // 1.2 若容量足够,则建立1个新的数组元素(Entry) 并放入到数组中--> 分析2 createEntry(hash, key, value, bucketIndex); } /** * 分析1:resize(2 * table.length) * 做用:当容量不足时(容量 > 阈值),则扩容(扩到2倍) */ void resize(int newCapacity) { // 1. 保存旧数组(old table) Entry[] oldTable = table; // 2. 保存旧容量(old capacity ),即数组长度 int oldCapacity = oldTable.length; // 3. 若旧容量已是系统默认最大容量了,那么将阈值设置成整型的最大值,退出 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } // 4. 根据新容量(2倍容量)新建1个数组,即新table Entry[] newTable = new Entry[newCapacity]; // 5. 将旧数组上的数据(键值对)转移到新table中,从而完成扩容 ->>分析1.1 transfer(newTable); // 6. 新数组table引用到HashMap的table属性上 table = newTable; // 7. 从新设置阈值 threshold = (int)(newCapacity * loadFactor); } /** * 分析1.1:transfer(newTable); * 做用:将旧数组上的数据(键值对)转移到新table中,从而完成扩容 * 过程:按旧链表的正序遍历链表、在新链表的头部依次插入 */ void transfer(Entry[] newTable) { // 1. src引用了旧数组 Entry[] src = table; // 2. 获取新数组的大小 = 获取新容量大小 int newCapacity = newTable.length; // 3. 经过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中 for (int j = 0; j < src.length; j++) { // 3.1 取得旧数组的每一个元素 Entry<K,V> e = src[j]; if (e != null) { // 3.2 释放旧数组的对象引用(for循环后,旧数组再也不引用任何对象) src[j] = null; do { // 3.3 遍历 以该数组元素为首 的链表 // 注:转移链表时,因是单链表,故要保存下1个结点,不然转移后链表会断开 Entry<K,V> next = e.next; // 3.4 从新计算每一个元素的存储位置 int i = indexFor(e.hash, newCapacity); // 3.5 将元素放在数组上:采用单链表的头插入方式 = 在链表头上存放数据 = 将数组位置的原有数据放在后1个指针、将需放入的数据放到数组位置中 // 即 扩容后,可能出现逆序:按旧链表的正序遍历链表、在新链表的头部依次插入 e.next = newTable[i]; newTable[i] = e; // 3.6 访问下1个Entry链上的元素,如此不断循环,直到遍历完该链表上的全部节点 e = next; } while (e != null); // 如此不断循环,直到遍历完数组上的全部数据元素 } } } /** * 分析2:createEntry(hash, key, value, bucketIndex); * 做用: 若容量足够,则建立1个新的数组元素(Entry) 并放入到数组中 */ void createEntry(int hash, K key, V value, int bucketIndex) { // 1. 把table中该位置原来的Entry保存 Entry<K,V> e = table[bucketIndex]; // 2. 在table中该位置新建一个Entry:将原头结点位置(数组上)的键值对 放入到(链表)后1个节点中、将需插入的键值对 放入到头结点中(数组上)-> 从而造成链表 // 即 在插入元素时,是在链表头插入的,table中的每一个位置永远只保存最新插入的Entry,旧的Entry则放入到链表中(即 解决Hash冲突) table[bucketIndex] = new Entry<>(hash, key, value, e); // 3. 哈希表的键值对数量计数增长 size++; }
此处有2点需特别注意: 键值对的添加方式 & 扩容机制
扩容过程当中的转移数据示意图以下
在扩容 resize() 过程当中,在将旧数组上的数据 转移到 新数组上时,转移操做 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的状况
设从新计算存储位置后不变,即扩容前 = 1->2->3,扩容后 = 3->2->1
此时若(多线程)并发执行 put()操做,一旦出现扩容状况,则 容易出现 环形链表 ,从而在获取数据、遍历链表时 造成死循环(Infinite Loop),即 死锁的状态 = 线程不安全
下面最后1节会对上述状况详细说明
至此,关于 “向 HashMap 添加数据(成对 放入 键 - 值对)“讲解完毕
/** * 函数原型 * 做用:根据键key,向HashMap获取对应的值 */ map.get(key); /** * 源码分析 */ public V get(Object key) { // 1. 当key == null时,则到 以哈希表数组中的第1个元素(即table[0])为头结点的链表去寻找对应 key == null的键 if (key == null) return getForNullKey(); --> 分析1 // 2. 当key ≠ null时,去得到对应值 -->分析2 Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); } /** * 分析1:getForNullKey() * 做用:当key == null时,则到 以哈希表数组中的第1个元素(即table[0])为头结点的链表去寻找对应 key == null的键 */ private V getForNullKey() { if (size == 0) { return null; } // 遍历以table[0]为头结点的链表,寻找 key==null 对应的值 for (Entry<K,V> e = table[0]; e != null; e = e.next) { // 从table[0]中取key==null的value值 if (e.key == null) return e.value; } return null; } /** * 分析2:getEntry(key) * 做用:当key ≠ null时,去得到对应值 */ final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } // 1. 根据key值,经过hash()计算出对应的hash值 int hash = (key == null) ? 0 : hash(key); // 2. 根据hash值计算出对应的数组下标 // 3. 遍历 以该数组下标的数组元素为头结点的链表全部节点,寻找该key对应的值 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; // 若 hash值 & key 相等,则证实该Entry = 咱们要的键值对 // 经过equals()判断key是否相等 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }
至此,关于 “向 HashMap 获取数据 “讲解完毕
即 对其他使用 API (函数、方法)的源码分析
void clear(); // 清除哈希表中的全部键值对 int size(); // 返回哈希表中全部 键值对的数量 = 数组中的键值对 + 链表中的键值对 boolean isEmpty(); // 判断HashMap是否为空;size == 0时 表示为 空 void putAll(Map<? extends K, ? extends V> m); // 将指定Map中的键值对 复制到 此Map中 V remove(Object key); // 删除该键值对 boolean containsKey(Object key); // 判断是否存在该键的键值对;是 则返回true boolean containsValue(Object value); // 判断是否存在该值的键值对;是 则返回true
下面,用3个图总结整个源码内容:
总结内容 = 数据结构、主要参数、添加 & 查询数据流程、扩容机制
数据结构 & 主要参数
添加 & 查询数据流程
扩容机制
HashMap 的实如今 JDK 1.7 和 JDK 1.8 差异较大,具体区别以下
JDK 1.8 的优化目的主要是:减小 Hash 冲突 & 提升哈希表的存、取效率;
8.2 为何HashMap具有下述特色:键-值(key-value)都容许为空、线程不安全、不保证有序、存储位置随时间变化
下面主要讲解 HashMap 线程不安全的其中一个重要缘由:多线程下容易出现 resize() 死循环
本质 = 并发 执行 put() 操做致使触发 扩容行为,从而致使 环形链表,使得在获取数据遍历链表时造成死循环,即 Infinite Loop
先看扩容的源码分析 resize()
关于resize()的源码分析已在上文详细分析,此处仅做重点分析:transfer()
/** * 源码分析:resize(2 * table.length) * 做用:当容量不足时(容量 > 阈值),则扩容(扩到2倍) */ void resize(int newCapacity) { // 1. 保存旧数组(old table) Entry[] oldTable = table; // 2. 保存旧容量(old capacity ),即数组长度 int oldCapacity = oldTable.length; // 3. 若旧容量已是系统默认最大容量了,那么将阈值设置成整型的最大值,退出 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } // 4. 根据新容量(2倍容量)新建1个数组,即新table Entry[] newTable = new Entry[newCapacity]; // 5. (重点分析)将旧数组上的数据(键值对)转移到新table中,从而完成扩容 ->>分析1.1 transfer(newTable); // 6. 新数组table引用到HashMap的table属性上 table = newTable; // 7. 从新设置阈值 threshold = (int)(newCapacity * loadFactor); } /** * 分析1.1:transfer(newTable); * 做用:将旧数组上的数据(键值对)转移到新table中,从而完成扩容 * 过程:按旧链表的正序遍历链表、在新链表的头部依次插入 */ void transfer(Entry[] newTable) { // 1. src引用了旧数组 Entry[] src = table; // 2. 获取新数组的大小 = 获取新容量大小 int newCapacity = newTable.length; // 3. 经过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中 for (int j = 0; j < src.length; j++) { // 3.1 取得旧数组的每一个元素 Entry<K,V> e = src[j]; if (e != null) { // 3.2 释放旧数组的对象引用(for循环后,旧数组再也不引用任何对象) src[j] = null; do { // 3.3 遍历 以该数组元素为首 的链表 // 注:转移链表时,因是单链表,故要保存下1个结点,不然转移后链表会断开 Entry<K,V> next = e.next; // 3.3 从新计算每一个元素的存储位置 int i = indexFor(e.hash, newCapacity); // 3.4 将元素放在数组上:采用单链表的头插入方式 = 在链表头上存放数据 = 将数组位置的原有数据放在后1个指针、将需放入的数据放到数组位置中 // 即 扩容后,可能出现逆序:按旧链表的正序遍历链表、在新链表的头部依次插入 e.next = newTable[i]; newTable[i] = e; // 访问下1个Entry链上的元素,如此不断循环,直到遍历完该链表上的全部节点 e = next; } while (e != null); // 如此不断循环,直到遍历完数组上的全部数据元素 } } }
从上面可看出:在扩容 resize() 过程当中,在将旧数组上的数据 转移到 新数组上时, 转移数据操做 = 按旧链表的正序遍历链表、在新链表的头部依次插入 ,即在转移数据、扩容后,容易出现 链表逆序的状况
设从新计算存储位置后不变,即扩容前 = 1->2->3,扩容后 = 3->2->1
注:因为 JDK 1.8 转移数据操做 = 按旧链表的正序遍历链表、在新链表的尾部依次插入 ,因此不会出现链表 逆序、倒置 的状况,故不容易出现环形链表的状况。
但 JDK 1.8 仍是线程不安全,由于 无加同步锁保护