|
|
简介
HashMap
是一种基于哈希表(hash table)实现的map,数组中每一个元素都是一个链表,把数组中的每一格称为一个 bin 或 bucket。
HashTable
的实现和 HashMap
非常相似,不过 HashTable 对每个方法进行了 Synchronize 同步以保证在多线程场景下的数据一致性,这种实现比较低效。在多线程场景下可以使用 Map m = Collections.synchronizedMap(new HashMap(...));
或并发包中的 ConcurrentHashMap
。
设计原理
桶 + 链表 + 红黑树
由于每一个桶中都是一个单向链表,hash相同的键值对都会作为一个节点被加入这个链表。当桶中的键值对数量过多时,会将桶中的单向链表转化为一个树。通过TREEIFY_THRESHOLD、UNTREEIFY_THRESHOLD和MIN_TREEIFY_CAPACITY来控制转换需要的阈值。
在JDK 8之前的 HashMap 中都只是采取了单向链表的方式,哈希碰撞会给查找带来灾难性的影响。在最差的情况下,HashMap 会退化为一个单链表,查找时间由 O(1) 退化为 O(n) 。而在JDK 8中,如果单链表过长则会转换为一颗红黑树,使得最坏情况下查找的时间复杂度为 O(log n) 。红黑树节点的空间占用相较于普通节点要高出许多,通常只有在比较极端的情况下才会由单链表转化为红黑树。
哈希表(也叫关联数组)一种通用的数据结构。
哈希表的概念:key经过hash函数后得到一个槽(buckets或slots)的索引,槽中保存着想要获取的值。伴随哈希表的还有哈希冲突,也就是一些key经过同一hash函数后可能产生相同的索引。在使用哈希表这种数据结构实现具体类时,需要注意:(1)设计好的hash函数,尽量减少冲突;(2)解决冲突发生后如何处理。
装填因子(load factor)为散列表中的元素个数对该表大小的比。
特点
- 线程非安全,并且允许key和value为null,HashTable的键值都不能为null。
- 不保证内部元素的顺序,随着元素增加,同一元素的位置也可能改变(resize的情况)。
- 遍历集合的时间复杂度与其容量(capacity, 槽的个数)和现有元素的大小(entry的个数)成正比
源码分析
属性
默认初始容量(槽的数量):16,容量必须为2的整数次幂
|
|
构造方法
HashMap的构造方法默认容量为16,table数组初始化采用了延迟加载的方式,直到第一次调用put方法时才会真正地分配数组空间。
|
|
节点
链表节点Node
HashMap的Entry定义,其实就是单向链表的一个节点Node,实现了Map.Entry接口,包括了键、值以及下一个节点的引用。这个Node节点是在普通的桶中使用的,在树形桶中使用TreeNode。
|
|
红黑树节点TreeNode
树形桶中使用的节点TreeNode
是LinkedHashMap.Entry
的子类,而LinkedHashMap.Entry
又是HashMap.Node
的子类。
使用TreeNode构造一颗红黑树,在红黑树中查找时可以发挥二叉查找的优势。当然,由于TreeNode也可以被当做普通节点Node的扩展,红黑树也可以按照单链表的方式进行遍历。
|
|
方法
hash算法
确定哈希桶数组的索引位置是关键的一步,通过hash算法求得这个位置。
在计算哈希值时,使用key对象自身的哈希值,并让高位和低位进行异或操作。
jdk1.7,使用indexFor来定位哈希桶数组的索引位置。
|
|
HashMap的Hash算法本质上就是三步:取key的hashCode值、高位参与运算、取模运算。
方法一其实是优化单纯使用key的hashCode值实现的Hash算法,其中第二步高位参与运算则是优化的核心。
记得在数据结构的《算法》散列表那部分看到过这么一个说法:保证散列均匀性的最好办法也许就是保证键的每一位都在散列值得计算中起到了作用,实现散列函数最常见的错误也许就是忽略了键的高位。
HashMap的取模运算并不是利用%
来实现的(取模运算消耗比较大),HashMap实现的方法h& (table.length-1)
来得到相对应得table索引值。HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (table.length-1)
运算等价于对length取模,也就是h%length
,但是&
比%
具有更高的效率。
$a \% (2^n)$ 等价于 $a \& (2^n - 1)$
tableSizeFor
table数组的大小要求是2的幂次方,如果构造方法中指定的容量并不是2的幂次方
put添加及更新
|
|
扩容
- 首先是调整capacity和threshold,非边际情况下,都变为原来的两倍。
- 将oldTable的元素逐个迁移到newTable里。
- 迁移优化
jdk1.7中需要重新计算每个元素的hash值和元素在数组中的位置。但是在jdk1.8中做了优化,因为capacity总是扩展为原来的两倍,只需要看看原来的hash值新增的那个bit是1还是0就可以,是0的话索引没变,是1的话索引变成“原索引+oldCap”。
|
|
查找
|
|
删除
|
|
遍历
HashMap提供了三种方式遍历其中的Entry、Key和Value,分别是Set<Map.Entry<K, V>> entrySet()
,Set<K> keySet()
和Collection<V> values()
。
迭代器
|
|
遍历方法
|
|
感谢:
http://blog.jrwang.me/2016/java-collections-hashmap/
http://liujiacai.net/blog/2015/09/03/java-hashmap/
http://tech.meituan.com/java-hashmap.html
http://www.importnew.com/20321.html