java学习_part01_java核心卷_day06_HashMap
参考:
1. 数据结构内部属性
1.1 存储数据结构
-
jdk1.8以前:数组+链表(增删快,查询慢)
-
jdk1.8开始:数组+链表+红黑树(增删快,查询也快)
注:
- 只有在链表长度大于等于8,数组长度大于等于64,才会把链表转化为红黑树,红黑树节点数小于6时,又变回链表
- 频繁扩容会导致红黑树进行拆分和重组,比较耗时,因此只在链表长度较长时候转化成红黑树才会显著提高效率
1.2 静态内部类Node
哈希桶数组Node[] table,实现了Map.Entry<K, V>接口,本质是一个键值对
属性:
- final int hash:存储key的hashcode经过扰动函数以后的hash值
- final K key
- V value
- Node<K, V> next:存储下一个节点的索引
方法:
- hashCode():键的哈希值与值的哈希值进行异或运算的结果
1.3 属性
- 序列化值
- 默认数组容量:16,初始化时机为第一次调用put方法,进一步调用resize方法时候执行
- 最大容量:1 <<30
- 默认负载因子:0.75
- 树化阈值:8,即链表添加元素后,该条链表长度如果大于等于8,且数组容量大于等于64,则树化,如果数组容量小于64,则扩容
- 反树化阈值:6,即红黑树节点数小于6,则转变成链表
- Node<K, V> table:初始化的数组用于存放Node
- int size:数组中实际键值对映射数目(即添加一个Node节点就+1)
- int threshold:扩容阈值,默认为数组容量*负载因子,哈希表中size大于该值就会扩容
2. 方法
2.1 构造方法
- public HashMap(int initialCapacity)
- pblic HashMap()
- public HashMap(Map<? extends K, ? extends V> m)
- pblic HashMap(int initialCapacity, float loadFactor):
- 如果初始化容量小于0,抛出异常
- 如果初始化容量大于最大值,则设置为最大值1<<30
- 如果负载因子不大于0或不是浮点数,抛出异常
- 否则,设置负载因子,设置初始化容量(大于等于传入参数的最小2次幂数)
2.2 tableSizeFor方法理解
参考:
求不小于传入参数的最小二次幂
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) ? MAXIMUM_CAPACITY : n + 1;
}
假定n为:
0b01xx xxxx xxxx xxxx...,右移1位,异或,得到
0b011x xxxx xxxx xxxx...,右移2位,异或,得到
0b0111 1xxx xxxx xxxx...,右移4位,异或,得到
0b0111 1111 1xxx xxxx...,右移一位,异或,得到
最后可以把最高位以后的数全变为1
2.2 put
-
调用put传入键值对,实际调用putVal(hash(key), key, value, false, true)
-
调用hash方法(扰动函数),对键的hashcode进行二次操作(经过高低位异或操作)得到hash值,构建Node对象{hash=hash值, k, v, next=null},对(数组长度-1)进行取余数操作,得到在数组中的位置,
-
hash方法(扰动函数)代码:
static final int hash(Object key){ int h; return(key == null ? :0 :(h=key.hashCode() ^ (h >>> 16))); }
-
结果取余数运算,就是与数组长度-1进行与运算,位运算效率高:
在数组中位置 = node.hash & (table.length() - 1)
-
-
如果未发生冲突,直接存放
-
如果发生了冲突,判断当前位置数据key是否相等,相等则覆盖,否则判断当前节点数据结构:
-
红黑树:存入红黑树节点
-
链表:判断当前键在链表中是否存在,存在就覆盖,否则判断插入之后链表大小是否大于8:
- 否:直接插入在链表末尾
-
-
是:判断数组大小是否大于64:
- 是:转化为红黑树
- 否:扩容
- 是:转化为红黑树
-
补充
-
为什么不直接用key的hashcode,而要用hash求hash值:
key的hashcode从-20亿到+20亿有40多亿的映射空间,只要哈希函数映射的比较均匀,一般很难出现碰撞.但是40长度数组,内存 放不下.,且需要进行对数组长度-1取模操作,余数才能访问下标
-
为什么要采用高16位与低16位进行异或操作:
扰动函数,高位与低位做异或是为了加大低位的随机性.且保留了高位的特征信息
-
jdk1.8相对于jdk1.7对HashMap的优化:
- 添加了红黑树结构
- 头插法变为尾插法
- 1.7扩容时候需要对原数组中元素进行重新hash定位在新数组位置,1.8采用简单的判断逻辑,位置不变或新位置下标=旧位置+旧数组大小
- 1.7为先判断是否需要扩容,再插入,1.8为先插入,再判断是否需要扩容
-
3. 扩容
3.1 扩容时机
- 空参数的构造方法,实例化的HashMap默认内部数组为null,没有实例化,第一次调用put时开始第一次初始化扩容,长度为16
- 有参数构造方法:用于指定容量.找到不小于传入参数的最小2次幂数,作为容量,确定扩容阈值=容量*负载因子
- 其他时候扩容:容量,阈值变为原来两倍
3.2 元素迁移
-
如果只有一个元素,以(新数组的长度-1)进行求余数确定新位置,实际上会在原地或移动旧数组个位置,与链表元素移动同理
-
如果当前为链表,举例:
假定旧的当前位置为1111,则其节点分为高位节点11111,低位节点01111,低位节点在新数组中的位置不变为01111,高位节点在新数 组中的位置变为11111
-
如果当前为红黑树:待添加...
4. 其他Map实现类(待补充)
4.1 HashTable(不建议使用,建议使用ConcurrentHashMap)
- 线程安全
- 不允许null作为键
- 初始容量为11,扩容时候为容量翻倍+1
- 计算hash时候为key的hashcode直接对数组长度取模运算