HashMap1.7底层原理
1.数据结构
HashMap1.7中底层数据结构采用数组+链表实现,也成为拉链法。
结构 | 描述 |
---|---|
数组(主) | 核心底层: table[ ],又称哈希数组 数组下标(indexFor):根据key值计算hash后与数组length-1进行&运算 数组元素(Entry):1个键值对=1个链表(头结点) 数组大小:capacity |
链表(副) | 每个链表(bucket):哈希表的桶 链表的节点值:一个键值对 链表长度:桶的大小 |
存储对象 | Entry类,属性包括:键(key),值(value),下一节点(next) |
2.存储流程
- 接收键值对
- 根据键值对中的key计算hash值
- 根据hash值计算存储存储的数组下标
- 判断该位置是否发生hash冲突?
- 若发生冲突,则将原位置的数据转移到链表中,并将键值对存储于数组中
- 若未发生冲突,则将键值对直接存储于该位置
3.使用api
V get(Object key); //获取指定键的值
V put(K key,V value); //添加键值对
void putAll(Map<? extends K, ? extends V> m); //将指定Map中的键值对 复制到 此Map中
V remove(Object key); //删除该键值对
boolean containsKey(Object key); //判断是否存在该键的键值对;是 则返回true
boolean containsValue(Object value); //判断是否存在该值的键值对;是 则返回true
Set<K> keySet(); //单独抽取key序列,将所有key生成一个Set
Collection<V> values(); //单独value序列,将所有value生成一个Collection
void clear(); //清除哈希表中的所有键值对
int size(); //返回哈希表中所有 键值对的数量 = 数组中的键值对 + 链表中的键值对
boolean isEmpty(); //判断HashMap是否为空;size == 0时 表示为 空
复制代码
4.重要参数
capacity:HashMap中数组的容量默认为16
static final int DEFAULT_INITIAL_CAPACITY = 1<<4;
static final int MAXIMUM_CAPACITY = 1 << 30;
loadFactor:加载因子默认为0.75f
final float loadFactor;
static final float DEFAULT_LOAD_FACTOR=0.75f
threshold:扩容阈值=容量*加载因子,当哈希表的大小>=扩容阈值时,哈希表进行扩容
int threshold;
table:存放key-value键值对的数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
size:存储键值对的数量(数组上键值对+所有链表上键值对)
transient int size;
复制代码
5.源码分析
1.构造参数
HashMap中存在4个构造函数,分别为:
- 默认构造函数(无参)
- 指定”容量大小”的构造函数
- 指定”容量大小”和”加载因子”的构造函数
- 包含”子Map”构造函数
HashMap<String,Integer> map = new HashMap<String,Integer>();
/*
* 构造函数1:默认构造函数(无参)
*/
public HashMap() {
// 实际上是调用构造函数3:指定"容量大小"和"加载因子"的构造函数
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
/*
* 构造函数2:指定"容量大小"的构造函数
*/
public HashMap(int initialCapacity) {
// 实际上调用指定"容量大小"和"加载因子"的构造函数
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/*
* 构造函数3:指定"容量大小"和"加载因子"的构造函数
*/
public HashMap(int initialCapacity, float loadFactor) {
// HashMap的最大容量只能是MAXIMUM_CAPACITY
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 设置 加载因子
this.loadFactor = loadFactor;
// 设置 扩容阈值 = 初始容量
// 注:此处不是真正的阈值,是为了扩展table,该阈值后面会重新计算
threshold = initialCapacity;
init(); // 一个空方法用于未来的子对象扩展
}
/*
* 构造函数4:包含"子Map"的构造函数
* 即 构造出来的HashMap包含传入Map的映射关系
* 加载因子 & 容量 = 默认
*/
public HashMap(Map<? extends K, ? extends V> m) {
// 设置容量大小 & 加载因子 = 默认
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
// 该方法用于初始化 数组 & 阈值,下面会详细说明
inflateTable(threshold);
// 将传入的子Map中的全部元素逐个添加到HashMap中
putAllForCreate(m);
}
复制代码
2.put()方法流程
- 判断table是否初始化,如未初始化则进行初始化
- 判断key值是否为null,若为null,则将该键-值存放到数组table中的第1个位置
- 若不为null,则根据key值计算出hash值
- 根据key的hash值与(table.length-1)进行与操作获得key对应存放的数组Table中位置
- 判断数组Table中位置是否存在该key,若存在,则使用新value替换旧value
- 若不存在,则将该key-value添加入table中
map.put("张三",1);
public V put(K key, V value)
/*
* 若哈希表未初始化(即table为空)table
* 则使用构造函数时设置的阈值(即初始容量)初始化数组
*/
if(table == EMPTY_TABLE){
inflateTable(threshold); //方法1
}
/*
* 判断key是否为空值null
* 若key == null,则将该键-值存放到数组table中的第1个位置,即table[0]
*(本质:key = Null时,hash值 = 0,故存放到table[0]中)
*/
if (key == null)
return putForNullKey(value); //方法2
/*
* 若 key ≠ null,则计算存放数组table中的位置(下标、索引)
* 根据键值key计算hash值后根据hash值与(数组.length-1)进行
* 与操作最终获得key对应存放的数组Table中位置
*/
int hash = hash(key); //方法3
int i = indexFor(hash,table.length); //方法4
/*
* 判断该key对应的值是否已存在(通过遍历以该数组元素为头结点的链表逐个判断)
*/
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//若该key已存在(即key-value已存在),则用新value替换旧value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue; //返回旧的value
}
}
modCount++;
//若该key不存在,则将"key-value"加到table中
addEntry(hash, key, value, i); //方法5
return null;
}
方法1:inflateTable(threshold)
private void inflateTable(int toSize){
int capacity = roundUpToPowerOf2(toSize); //将传入的容量转化为大于容量的最小二次幂
threshold = (int)Math.min(capacity*loadFactor,MAXIMUM_CAPACITY + 1); //重新计算阈值
table = new Entry[capacity]; //用大于容量的最小二次幂作为容量初始化table
}
private static int roundUpToPowerOf2(int number){
//若number大于MAXIMUM_CAPACITY则返回MAXIMUM_CAPACITY否则返回传入容量大小的最小的2的次幂
return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY :
(number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
方法2:putForNullKey(value)
private V putForNullKey(V value){
// 遍历以table[0]为首的链表,寻找是否存在key==null 对应的键值对
// 若有:则用新value替换旧value;同时返回旧的value值
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null){
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
//若没有,则调用addEntry(),将空键 & 对应的值封装到Entry中,并放到table[0]
addEntry(0,null,value,0);
}
方法3:hash(key)
JDK1.7中做了9次扰动处理 = 4次位运算 + 5次异或运算
static final int hash(int h){
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
JDK 1.8 简化了扰动函数 = 只做了2次扰动 = 1次位运算 + 1次异或运算
static final int hash(Object key){
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
方法4:indexFor(hash,table.length)
static int indexFor(int h,int length){
//将对哈希码扰动处理后的结果与运算(&)(数组长度-1)
//得到存储在数组table的位置(即数组下标、索引)
return h & (length-1);
}
方法5:addEntry(hash, key, value, i)
void addEntry(int hash, K key, V value, int bucketIndex){
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容2倍
resize(2 * table.length);
//重新计算该Key对应的hash值
hash = (null != key) ? hash(key) : 0;
//重新计算Key对应的hash值的存储数组下标
bucketIndex = indexFor(hash, table.length);
}
//若容量足够,则创建1个新的数组元素(Entry) 并放入到数组中
createEntry(hash, key, value, bucketIndex); //方法6
}
方法6:createEntry(hash, key, value, bucketIndex);
void createEntry(int hash, K key, V value, int bucketIndex) {
//把table中该位置原来的Entry保存
Entry<K,V> e = table[bucketIndex];
//在table中该位置新建一个Entry:将原头结点位置(数组上)的键值对 放入到(链表)后1个节点中、将需插入的键值对 放入到头结点中(数组上)-> 从而形成链表
//即在插入元素时,是在链表头插入的,table中的每个位置永远只保存最新插入的Entry,旧的Entry则放入到链表中(即解决Hash冲突)
table[bucketIndex] = new Entry<>(hash, key, value, e);
//哈希表的键值对数量计数增加
size++;
}
复制代码
3.resize()方法流程
- 插入键值对时,发现容量不足
- 保存旧的数组
- 根据新容量(2倍)新建数组
- 遍历旧数组的每个数据并计算每个数据在新数组中的存储位置
- 将旧数组上的每个数据逐个转移到新数组中
- 将新数组table引用到HashMap的table属性上
- 重新设置扩容阈值
void resize(int capacity){
//保存旧数组
Entry[] oldTable = table;
//保存旧容量(old capacity),即数组长度
int oldCapacity = oldTable.length;
// 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//根据新容量(2倍容量)新建1个数组,即新table
Entry[] newTable = new Entry[newCapacity];
//将旧数组上的数据(键值对)转移到新table中,从而完成扩容
transfer(newTable); //方法1
//将新数组table引用到HashMap的table属性上
table = newTable;
//重新设置阈值
threshold = (int)(newCapacity * loadFactor);
}
方法1:transfer(newTable)
void transfer(Entry[] newTable){
//src引用旧数组
Entry[] src = table;
//获取新数组容量
int newCapacity = newTable.length;
//遍历旧数组,将旧数组上的数据(键值对)转移到新数组中
for(int j = 0; j < src.length; j++){
//获取旧数组上的每个元素
Entry<K,V> e = src[j];
if (e != null){
//释放旧数组的对象引用
src[j] = null;
do{
// 遍历以该数组元素为首的链表
// 注:转移链表时,因是单链表,故要保存下1个结点,否则转移后链表会断开
Entry<K,V> next = e.next;
//重新计算每个元素的存储位置
int i = indexFor(e.hash, newCapacity);
//将元素放在数组上:采用单链表的头插入方式
e.next = newTable[i];
newTable[i] = e;
//访问下一个Entry链上的元素,如此不断循环,直到遍历完该链表上的所有节点
e = next;
}while(e != null)
}
}
}
复制代码
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END