HashMap源码理解
时间:2022-04-27
本文章向大家介绍HashMap源码理解,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
private static int roundUpToPowerOf2(int number) {
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
- Integer.highestOneBit方法是获取不大于本身的2^n的值,那该方法具体含义是:
- 获取新的数组容量值,如果给定值大于等于最大的容量,则返回最大容量,否则:如果容量小于等于1,则返回1,否则返回大于等于给定值的最接近的2^n的值
- 容量为什么要是2^n次方呢?且看如下代码:
-
- 给方法是查找h(ash)在数组的索引位置;那现在看length为什么要是2^n次方呢?点此查看
- 保证&之后的数据不大于length
- length-1之后,最低的n位都是1,那与h的&运算之后的值即是h的最低n位
- 采用length-1而不是直接length是因为2^n最低一位是0,那&运算之后数据都分布在偶数位,不是随机的
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
扩容:先获取最接近并且>=指定容量的的数值作为扩容后的容量;扩容界限就是取容量*加载因子和最大容量+1的最小值;重新初始化table;扩容之后的table没有任何数据,所以在相关调用操作之后会有数据重新分配操作,比较耗时,所以在初始化一个hashmap的时候最好指定容量避免扩容操作发生
public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
get方法:
- 如果key==null,则单独获取
private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
- 如果size==0,则直接返回null;
- 遍历table,判断key,返回value
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
根据key获取Entry:
- 获取hash值
- 获取数组索引
- 获取数组中索引的第一个entry
- 遍历entry,如果hash值相等并且(key相等或者equal),则返回当前entry即可
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
判断key是否存在只是调用getEntry方法判断是否为null即可
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
put方法:
- 如果table为空数组,则先扩容到扩容界限(threshold)的数组,如果是默认的初始化方法,则threshold=容量;执行该方法之后。threshold=容量*加载因子;
- 如果key==null,单独存入该值,putForNullKey
- 根据hash和length获取table索引,找出第一个entry
- 遍历entry,如果找到则重新设置新值,返回旧值,否则新添加一个entry(addEntry)
private V putForNullKey(V 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;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
- 存入key==null的value,直接查找table的第一个位置(index=0),如果找到则重新设置新值,返回旧值,否则添加新的entry;
- key==null的值全部在table【0】的entry上
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
创建新的entry:
- 先检查是否满足扩容条件:size>=threshold&&null!=table[bucketIndex];
- 如果满足扩容,计算新的hash和数组索引(bucketIndex)
- 创建新的entry(createEntry)
- 注意事项:size是针对hash表里的所有数据的容量,而扩容是指数组扩容
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
- 先获取oldTable的length,如果已是最大长度,则无需扩容,并且将threshold设为Integer.MAX_VALUE,不可扩容!
- 创建新的Entry数组,将旧的table数据添加到newTable中(transfer)
- 设置新的table和threshold
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
简单理解为:数据拷贝
- 获取新的数组长度,遍历table数组;
- 遍历每个链表:重定向到新的数组和链表中
- 现在遍历到[1,0]即table[1]下的第一个entry,计算得出在新的i=5,则该entry的下一个是newTable[5],newTable[5]=entry
- 从以上可以看出,新进入newTable的数据在后进入的next下
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
创建新的Entry:
- 先根据索引(bucketIndex)获取数组中的元素(e)
- 创建新的entry,位于链表第一个entry,而e是当前新的entry的next
public void putAll(Map<? extends K, ? extends V> m) {
int numKeysToBeAdded = m.size();
if (numKeysToBeAdded == 0)
return;
if (table == EMPTY_TABLE) {
inflateTable((int) Math.max(numKeysToBeAdded * loadFactor, threshold));
}
/*
* Expand the map if the map if the number of mappings to be added
* is greater than or equal to threshold. This is conservative; the
* obvious condition is (m.size() + size) >= threshold, but this
* condition could result in a map with twice the appropriate capacity,
* if the keys to be added overlap with the keys already in this map.
* By using the conservative calculation, we subject ourself
* to at most one extra resize.
*/
if (numKeysToBeAdded > threshold) {
int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1);
if (targetCapacity > MAXIMUM_CAPACITY)
targetCapacity = MAXIMUM_CAPACITY;
int newCapacity = table.length;
while (newCapacity < targetCapacity)
newCapacity <<= 1;
if (newCapacity > table.length)
resize(newCapacity);
}
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
put(e.getKey(), e.getValue());
}
将一个集合添加到现有集合中:
- 先获取要添加结合的元素数量
- 如果现有集合是空集合,扩容:当前threshold和根据添加元素容量计算的新容量的最大值
- 假如添加的集合元素数量>threshold,则判断当前table是否需要扩容
- 此处是保守估计新的集合添加之后的容量:但是可以保证最多只有一次调用resize方法!
- 如果numKeysToBeAdded <=threshold,即使在put方法导致扩容也至多有一次:扩容至两倍,那threshold也会变为两倍
- 如果numKeysToBeAdded >threshold,如果:targetCapacity>table.length,则在put方法可能会导致resize,否则newCapacity必定大于table.length,在此处resize,put方法就不会resize了
- IE7下元素的 'padding-top' 遇到 'clear' 特性在某些情况下复制到 'padding-bottom'
- IE7下元素的 'padding-top' 遇到 'clear' 特性在某些情况下复制到 'padding-bottom'
- ARM处理器:开放者的逆袭
- 从5个方面对比微信小程序和App
- ActiveMQ笔记(7):如何清理无效的延时消息?
- JS魔法堂:再识Bitwise Operation & Bitwise Shift
- Hadoop(十三)分析MapReduce程序
- mac机上搭建php56/nginx 1.8.x/thinkphp 3.2.x/gearman扩展/seaslog扩展/redis扩展环境
- 基础野:细说无符号整数
- Ubuntu12.04安装QQ for Linux
- 树莓派:最好的安排
- idea 高级调试技巧
- JS魔法堂:彻底理解0.1 + 0.2 === 0.30000000000000004的背后
- mybatis: 利用多数据源实现分库存储
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- (数据科学学习手札90)Python+Kepler.gl轻松制作时间轮播地图
- WEB前端安全自查和加固
- 硬核!IDEA 中使用 Git 完整入门教程!
- 神器你值得拥有——CoolFormat代码一键自动格式化工具,支持Verilog
- 一个简单现实案例挑战 PowerBI 水平测试 - 深度解析
- [译]使用DOT语言和GraphvizOnline来可视化你的ASP.NETCore3.0终结点01
- 探索 React 内核:深入 Fiber 架构和协调算法
- [经验]使用Keil MDK+Jlink-OB下载失败的解决办法
- ESP8266两种工作模式数据传输测试
- 『真香警告』这33个超级好用的CSS选择器,你可能见都没见过。
- [踩坑]STM32外部8M晶体不起振会有什么现象?
- 干掉 GuavaCache:Caffeine 才是本地缓存的王
- 美团数据怎么爬,看看这个文章吧!
- [硬件]关于SPI Flash那些你不知道的事儿
- 「新特性」Spring Boot 全局懒加载机制了解一下