HashMap 是 Java 中 Map 的一个实现类,它是一个双列结构(数据+链表),这样的结构使得它的查询和插入效率都很高。HashMap 允许 null 键和值,它的键唯一,元素的存储无序,并且它是线程不安全的。
HashMap 的双列结构是数组 Node[]+链表,我们知道数组的查询很快,但是修改很慢,因为数组定长,所以添加或者减少元素都会导致数组扩容。而链表结构恰恰相反,它的查询慢,因为没有索引,需要遍历链表查询。但是它的修改很快,不需要扩容,只需要在首或者尾部添加即可。HashMap 正是应用了这两种数据结构,以此来保证它的查询和修改都有很高的效率。
HashMap 在调用 put() 方法存储元素的时候,会根据 key 的 hash 值来计算它的索引。HashMap 在计算索引的时候尽量保证它的离散,但还是会有不同的 key 计算出来的索引是一样的,那么第二次 put 的时候,key 就会产生冲突。HashMap 用链表的结构解决这个问题,当 HashMap 发现当前的索引下已经有不为 null 的 Node 存在时,会在这个 Node 后面添加新元素,同一索引下的元素就组成了链表结构。
HashMap 里数组的几个参数:
DEFAULT_INITIAL_CAPACITY,默认初始长度,16
MAXIMUM_CAPACITY,最大长度,2^30
DEFAULT_LOAD_FACTOR,默认加载因子,0.75
put 的逻辑
-
计算容量:根据 map 的 size 计算数组容量大小,如果元素数量也就是 size 大于数组容量 ×0.75,则对数组进行扩容,扩容到原来的 2 倍。
-
查找数据索引:根据 key 的 hash 值和数组长度找到 Node 数组索引。
-
储存:这里有以下几种情况(假设计算出的 hash 为 i,数组为 tab,变量以代码为例)
-
当前索引为 null,直接 new 一个 Node 并存到数组里,tab[i]=newNode(hash, key, value, null)
-
数组不为空,这时两个元素的 hash 是一样的,再调用 equals 方法判断 key 是否一致,相同,则覆盖当前的 value,否则继续向下判断
-
上面两个条件都不满足,说明 hash 发生冲突,Java 8 里实现了红黑树,红黑树在进行插入和删除操作时通过特定算法保持二叉查找树的平衡,从而可以获得较高的查找性能。本篇也是基于 Java 8 的源码进行分析,在这里 HashMap 会判断当前数组上的元素 tab[i] 是否是红黑树,如果是,调用红黑树的 putTreeVal 的 put 方法,它会将新元素以红黑树的数据结构储存到数组中。
如果以上条件都不成立,表明 tab[i] 上有其它 key 元素存在,并且没有转成红黑树结构,这时只需调用 tab[i].next 来遍历此链表,找到链表的尾然后将元素存到当前链表的尾部。
HashMap 的 get()
-
根据 hash 值和数组长度找到 key 对应的数组索引。
-
拿到当前的数组元素,也就是这个链表的第一个元素 first,先用 hash 和 equals() 判断是不是第一个元素,是的话直接返回,不是的话继续下面的逻辑。
-
不是链表的第一个元素,判断这个元素 first 是不是红黑树,如果是调用红黑树的 getTreeNode 方法来查询。
-
如果不是红黑树结构,从 first 元素开始遍历当前链表,直到找到要查询的元素,如果没有则返回 null。
HashMap 与 HashTable
-
HashMap 是线程不安全的,HashTable 线程安全,因为它在 get、put 方法上加了 synchronized 关键字。
-
HashMap 和 HashTable 的 hash 值是不一样的,所在的桶的计算方式也不一样。HashMap 的桶是通过 & 运算符来实现 (tab.length - 1) & hash,而 HashTable 是通过取余计算,速度更慢(hash & 0x7FFFFFFF) % tab.length (当 tab.length = 2^n 时,因为 HashMap 的数组长度正好都是 2^n,所以两者是等价的)
-
HashTable 的 synchronized 是方法级别的,也就是它是在 put() 方法上加的,这也就是说任何一个 put 操作都会使用同一个锁,而实际上不同索引上的元素之间彼此操作不会受到影响;ConcurrentHashMap 相当于是 HashTable 的升级,它也是线程安全的,而且只有在同一个桶上加锁,也就是说只有在多个线程操作同一个数组索引的时候才加锁,极大提高了效率。