目录
1. 基本介绍
(1)Map接口的常用实现子类:HashMap、Hashtable和Properties。
(2)HashMap是Map接口 使用频率最高的实现类。
(3)HashMap是以key-value对的方式 来存储数据。
(4)key不能重复,但是是值可以重复,允许使用null键和null值。
(5)如果添加相同的key,则会覆盖原来的key-value,相当于修改(key不会替换,value会替换)。
(6)与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的。
(7)HashMap没有实现同步,因此是线程不安全的。(方法没有做同步互斥的操作,没有synchronized修饰)
2. HashMap类 底层机制分析
2.1 示意图
①k-v是一个Node 实现了Map.Entry<K,V>,查看HashMap的源码可以看到。
②JDK 7.0的HashMap 底层实现(数组+链表),JDK 8.0底层实现(数组+链表+红黑树)。
2.2 扩容机制(和HashSet相同)
①HashMap底层维护了Node类型的数组table,默认为null。
②当创建对象时,将加载因子(loadfactor)初始化为0.75。
③当添加key-value时,通过key的hash值得到在table数组的索引。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断 该元素的key 和 准备加入的key 是否相等,如果相等,则直接替换value。如果不相等,需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
④第1次添加,则需要扩容table容量为16,临界值(threshold)为12。
⑤以后再扩容,则需要扩容table容量为原来的2倍,临界值为原来的2倍,即24,依次类推。
⑥在JDK 8中,如果一条链表的元素个数达到 TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64)就会进行树化(红黑树)。
3. HashMap类 底层机制 源代码分析
(1)执行new HashMap()后,调用HashMap的无参构造器,初始化加载因子loadFactor为0.75。执行完这条代码后,此时HashMap$Node[] table = null。
[第一次添加:put("java", 10)]
(2)执行put("java", 10)后,先调用valueOf()方法,对基本数据类型进行自动装箱操作。
(3)装箱完成后,就调用并执行put(K key, V value)方法。在该方法中会调用hash()方法,通过一个算法(不用深究),来计算出key的hash值。
(4)在得到key的hash值后,就进入到了putVal()方法。
//这个putVal()方法是和HashSet底层进入的同一个putVal()方法,每条代码的详细解读 去“HashSet底层机制 源代码分析”查看
(5)在putVal()方法中,进行第一个if语句的判断。首先判断table数组是否为null。
此时的table数组就是null,判断成功后,就进入if语句,调用resize()方法,对table数组扩容为16个空间,每个空间的值都是null。
//在resize()方法中,扩容的核心语句就是下面这句
(6)扩容后,就进行第二个if语句的判断。通过hash值得到一个table数组的索引位置的Node节点,判断这个节点是否为null。
此时因为整个table数组都是null,所以得到的Node肯定为空,就判断成功,进入if语句,创建一个newNode新的节点,将加入的key-value值放到对应的节点。
//执行完添加后,此时table数组中,索引为3的位置就有了新的节点
(7)执行完第二个if语句后,就不进入下面的else语句,直接来到了最后。执行++modCount语句后,修改次数加1。
然后if语句判断数组的元素个数是否超过threshold。此时数组元素只有一个,没有超过12个,不进入if语句,就在最后返回null,完成第一对key-value添加。
[第二次添加:put("php", 10)]
(8)执行put("php", 10)操作,进行第二对key-value添加。依然是先调用valueOf()方法进行自动装箱。然后调用put()方法,再执行hash(key)方法,计算key的hash值。最后进入putVal()方法。
(9)在putVal()方法中,进行第一个if语句的判断。因为此时table数组中已经有元素了,所以不为null,不会进入这个if语句。
(10)然后进行第二个if语句的判断。此时的hash值与第一次的不同,所以得到的table数组的索引位置的Node节点也不同,就应该还是null。进入这个if语句,创建一个newNode,将新的key-value值放到对应的节点。
//执行完添加后,此时table数组中,索引为9的位置就有了新的节点
(7)执行完第二个if语句后,同样不进入下面的else语句,直接来到了++modCount语句,修改次数加1。数组元素只有两个,没有超过12个,不进入if语句,就在最后返回null,完成第二对key-value添加。
[第三次添加:put("java", 20)]
(8)执行put("java", 20)操作,进行第三对key-value添加。依然是先调用valueOf()方法进行自动装箱。然后调用put()方法,再执行hash(key)方法,计算key的hash值。最后进入putVal()方法。
(9)在putVal()方法中,第一个if语句判断同样因为table数组中已经有元素了,不为null,不会进入这个if语句。
(10)然后进行第二个if语句的判断。此时加入的key是java,和第一次相同,所以计算出的hash值与第一次的相同。经过算法得到的索引位置也是第一次的位置,所以不为null,就不会进入这个if语句,而是进入else语句。
(11)在else语句中先进行第一个if语句进行判断。此时加入的元素的hash值和 数组中当前p指向位置的hash值相同,同时此时key的内容和数组中指向的内容都是java,所以就满足了这个if语句的判断,执行e = p,使e指向数组中的当前元素。
(12)在else语句中第一个if语句判断成功后,就不执行下面的else if和else语句,而执行第二个if语句判断。此时的e指向了数组中的当前元素,不为null,判断成功就进入这个if语句。
然后在里面的if语句也判断成功,执行e.value = value,将数组中当前指向元素的value = 10替换为此时加入的value = 20。执行完后就返回oldValue,完成第三次添加操作。
(13)三次添加都执行完后,此时的table数组中只有两对k-v,但第一对k-v的value值被替换为了第三次添加的value值。