深入理解hashmap
一,特点:
1 :非线程安全 (注:如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap)
2:有很快的访问速度,但遍历顺序却是不确定的
3:HashMap最多只允许一条记录的键为null,但是允许多条记录的值为nulll
二,内部实现:
2.1 从结构上来讲,hashmap 是数组 + 链表 + 红黑树 ( 在jdk1.8 中增加了红黑树部分) 进行实现的
看 hashmap 的源码
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
可以看到定义了Node<K , V> 上面每个小黑点就是 hashmap 的一个内部类。
2.2 hashmap 通过hash 表来进行存储, 但是hash表本身因为得出的hash 值可能相同,会造成hash 冲突,哈希表为了解决这个冲突,一般能够采用两种解决方法:开放地址法 和 链地址法。 在java 中使用了链地址法解决冲突,其实也就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上
比如
map.put("皮卡丘", "小智");
系统会算出"皮卡丘" 这个key 的 hashCode 方法,得到 hash 值,后再通过Hash算法的后两步运算(高位运算和取模运算,下文有介绍)来定位该键值对的存储位置,有时两个key会定位到相同的位置,表示发生了Hash碰撞。
当然,若是哈希桶的数组很大,这样产生碰撞的概率就低,hashmap 查找到对应数据的时间成本 就越低,但是这牺牲了 本就不多的内存空间,占用了空间成本。所以, 我们要保证这两者的平衡,采用好的hash算法 和 良好的扩容机制保证时间和空间的有效利用。
来看看源码中默认构造函数的初始参数
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;
/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;
/**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor;
/* ---------------- Public operations -------------- */
首先,Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
了解了hashmap 的基础概念后要注意两个注意点:
1: 在Hashmap 中 , 数组的长度必须是 2的n 次方, 就算我们手动输入一个非2 的n 次方的数,hashmap 会自动将其转为大于这个数的最接近的2的n 次方的数。 至于为啥呢? 接下来会有解释, 在这里先给一个概念: 为了在定位 哈希桶索引位置时做出的选择。
2:我们也发现了,尽管hash 算法设计得再合理, 也免不了产生hash 冲突的情况, 这造成了数据不断在同一个索引位置堆积( 以链表的形式) , 链表过长时,就会严重的影响hash map 的性能, 因此 在jdk 1.8 中,为了提高增删改查的性能 ,引入了红黑树 (当链表长度大于8时)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IYcn5v24-1610716659849)(C:\Users\86150\AppData\Roaming\Typora\typora-user-images\image-20210115174707502.png)]
当红黑树的节点数目小于 6 时, 转为链表。
本文主要从根据key获取哈希桶数组索引位置、put方法的详细执行、扩容过程三个具有代表性的点深入展开讲解
一:哈希数组的索引位置
首先 是hashmap的 初始化, 如果new hashmap( ) 中不传值, 默认大小 则是 16 , 负载因子 是 0.75 , 如果自己传入初始大小k,初始化大小为 大于k的 2的整数次方,例如如果传10,大小为16。 那么这个是怎么实现的 呢?(不妨 看看源码)
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY)