public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>{
代码片段是一个自定义的HashMap类,它扩展了AbstractMap类并实现了Map接口。HashMap是一种常用的数据结构,它使用键值对存储和检索数据。
在这个类中, <K,V> 表示泛型参数,它允许您在创建HashMap对象时指定键和值的类型。AbstractMap类提供了一些默认的实现,而Map接口定义了操作HashMap的方法。
通过扩展AbstractMap类和实现Map接口,您的HashMap类将继承和实现一些基本的方法和功能.
//默认容量 - 容量必须的2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
//最大容量 -- int类型取值范围内最大的2的幂的数字
static final int MAXIMUM_CAPACITY = 1 << 30;//1073741824
//默认的负载因子
//1 --》 16*1=16(阈值) -- 扩容数组慢(利用了空间,牺牲时间)
//0.1 --》 16*0.1=1(阈值) -- 扩容数组快(利用了时间,牺牲了空间)
//0.75 --> 16*0.75=12(阈值) -- 装12个数据就扩容(取得时间和空间的平衡)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//空内容的数组
static final Entry<?,?>[] EMPTY_TABLE = {};
//数组容器 -- hash表、hash数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;//{}
//元素个数(映射关系的个数)
transient int size;//0
//阈值(容量*负载因子)
int threshold;//16
//负载因子
final float loadFactor;//0.75
//外部操作数
transient int modCount;//0
//hash种子数
transient int hashSeed = 0;//0
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
这段代码看起来是一个构造函数,它创建了一个HashMap对象。这个构造函数使用了两个默认参数:DEFAULT_INITIAL_CAPACITY
和DEFAULT_LOAD_FACTOR
。
//initialCapacity - 16
//loadFactor - 0.75
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))//NaN - Not a Number
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
//toSize - 16
private void inflateTable(int toSize) {
//计算容量(获取toSize的二的幂的数字) -- 16
int capacity = roundUpToPowerOf2(toSize);
//计算阈值 - 12
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//初始化容器数组 -- new Entry[16]
table = new Entry[capacity];
//计算hash种子数
initHashSeedAsNeeded(capacity);
}
//number - 16
private static int roundUpToPowerOf2(int number) {
//Integer.highestOneBit(数字) -- 保留最高位的1,其余都是0
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY//用于限制结果不能超过2的30次方
: (number > 1) ? Integer.highestOneBit((number-1) << 1) : 1;
} //使用了Java内置的方法Integer.highestOneBit()来找到一个数字的最高位的1。通过将该数字 减1,然后将结果与原数字相或,并将结果左移1位,我们可以得到最接近原数字的2的幂次方。
private V putForNullKey(V value) {
//下标为0的位置有Entry对象,就意味着hash碰撞了
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {//如果将HashMap的只能设置为0,那么下表为0的key就不为空,这样就会将空的值存在第一个。
//获取老的value
V oldValue = e.value;
//替换value
e.value = value;
e.recordAccess(this);
return oldValue;//返回老的value
}
}
modCount++;
addEntry(0, null, value, 0);//把数据添加到Entry对象中,Entry对象添加table中
return null;
}
final int hash(Object k) {
int h = hashSeed;
//判断key是否是String,如果是,就计算hash值(hashSeed去参与计算hash值的工作)
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
//长度必须的2的幂,是因为要让元素散列均匀
//为了保证在进行哈希计算时,能够更有效地利用位运算来确定元素在新的数组中的位置。
/**HashMap内部使用哈希函数将键映射到数组的索引位置。当HashMap的容量达到负载因子(load factor) 所定义的阈值时,就会触发扩容操作。扩容操作的目的是为了减少哈希冲突,提高HashMap的性能。
在扩容过程中,HashMap会创建一个更大的数组,然后将原数组中的元素重新分配到新数组中。为了确定元 素在新数组中的位置,HashMap使用元素的哈希码(hash code)与新数组的长度进行位运算,计算出元素 在新数组中的索引位置。
当newsize不是2的幂时,进行位运算时无法保证结果的正确性。例如,如果newsize是一个奇数,那么在进 行位运算时,无法保证元素在新数组中的位置分布均匀,可能会导致哈希冲突增加,降低HashMap的性能。
因此,为了保证位运算的正确性和性能的最优化,HashMap在扩容时要求newsize必须是2的幂。这样可以通 过位运算来高效地计算元素在新数组中的位置,减少哈希冲突的发生,提高HashMap的效率和性能。
*/
//如果长度不为2的幂,会增加同一个下标上有多个元素的几率(hash碰撞),导致效率降低
return h & (length-1);
}
//key - new Student("李孙浩", '男', 22, "2301", "003")
//value - "美食"
public V put(K key, V value) {
//第一次添加元素时,进入的判断
if (table == EMPTY_TABLE) {
//初始化数据(阈值、hash种子、数组)
inflateTable(threshold);
}
//判断key是否是null,如果是null,就将数据添加至数组下标为0的位置
if (key == null)
return putForNullKey(value);
//获取key的hash值
int hash = hash(key);
//利用hash值计算在数组中的下标
int i = indexFor(hash, table.length);
//判断下标上是否有Entry对象
//进入该判断,就意味着hash碰撞(竟可能的避免 -- 重写key的hashCode和equals)
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))) {
//获取老的value
V oldValue = e.value;
//替换value值
e.value = value;
e.recordAccess(this);
//返回老的value
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
//hash - 20
//key - new Student("张浩天", '男', 23, "2301", "002")
//value - "打篮球"
//bucketIndex - 4
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);
}
//newCapacity - 32
void resize(int newCapacity) {
//获取老的table
Entry[] oldTable = table;
//获取老的长度 -- 16
int oldCapacity = oldTable.length;
//判断老的长度是否等于数组的最大值,如果等于数组的最大值,不能再扩容
if (oldCapacity == MAXIMUM_CAPACITY) {
//将int的最大值赋值给阈值,意味永远都无法扩容
threshold = Integer.MAX_VALUE;
return;
}
//创建新的数组
Entry[] newTable = new Entry[newCapacity];
//扩容 - 1.重新计算hashSeed 2.将老的数组中的数据复制到新数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//将新数组的地址赋值给老的数组的引用
table = newTable;
//重新计算阈值 threshold - 24
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
//newCapacity - 32
int newCapacity = newTable.length;
//遍历老的数组
for (Entry<K,V> e : table) {
//e - null
while(null != e) {
//next - null
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//计算Entry对象在新数组中的下标
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
//hash - 20
//key - new Student("张浩天", '男', 23, "2301", "002")
//value - "打篮球"
//bucketIndex - 4
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++;
}
//节点类/映射关系类
static class Entry<K,V> implements Map.Entry<K,V> {
final K key; ------ key
V value; ---------- value
Entry<K,V> next; -- 下一个节点的引用地址
int hash; --------- key的hash值
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
}
}
- 构造函数
HashMap(int initialCapacity, float loadFactor)
:接受初始容量和负载因子作为参数,并进行参数合法性检查。如果初始容量小于0,抛出IllegalArgumentException
异常;如果初始容量大于最大容量MAXIMUM_CAPACITY
,则将初始容量设置为最大容量;如果负载因子小于等于0或者是NaN(不是一个数字),则抛出IllegalArgumentException
异常。然后,将负载因子和阈值设置为参数值,并调用init()
方法进行初始化。 inflateTable(int toSize)
方法:根据给定的大小toSize
扩展哈希表的容量。首先,通过调用roundUpToPowerOf2(int number)
方法计算容量(将toSize
转为大于等于它的最小的2的幂次数)。然后,计算阈值,即容量乘以负载因子和最大容量加1的较小值。接下来,初始化哈希表数组table
为指定容量大小,并调用initHashSeedAsNeeded(int capacity)
方法计算哈希种子数。roundUpToPowerOf2(int number)
方法:将给定的数字number
转为大于等于它的最小的2的幂次数。如果number
大于等于最大容量MAXIMUM_CAPACITY
,则返回最大容量;否则,如果number
大于1,使用Integer.highestOneBit((number-1) << 1)
方法计算最小的2的幂次数;如果number
等于1,则返回1。putForNullKey(V value)
方法:将键为null的映射关系添加到哈希表中。首先,遍历数组下标为0的位置,查找是否已经存在键为null的映射关系。如果存在,替换其值并返回旧值;如果不存在,调用addEntry(int hash, K key, V value, int bucketIndex)
方法将新的映射关系添加到数组下标为0的位置。hash(Object k)
方法:计算给定键k
的哈希值。首先,将哈希种子数赋值给h
。如果哈希种子数不为0且键k
是一个字符串类型,调用sun.misc.Hashing.stringHash32((String) k)
方法计算字符串的哈希值。否则,将k.hashCode()
的结果与h
异或,并对结果进行一系列位运算操作,最后返回结果。indexFor(int h, int length)
方法:根据给定的哈希值h
和数组长度length
,计算哈希值在数组中的下标。这里要求数组长度必须是2的幂次数,这样可以保证元素在数组中分布均匀,减少哈希冲突的几率。put(K key, V value)
方法:将给定的键值对添加到哈希表中。首先判断哈希表是否为空,如果为空,则调用inflateTable(int toSize)
方法进行初始化。然后,判断键是否为null,如果是null,则调用putForNullKey(V value)
方法将映射关系添加到数组下标为0的位置。如果键不为null,计算键的哈希值和在数组中的下标。接着,遍历数组对应下标位置的链表,查找是否已经存在相同的键。如果存在,替换其值并返回旧值;如果不存在,调用addEntry(int hash, K key, V value, int bucketIndex)
方法将新的映射关系添加到链表中。addEntry(int hash, K key, V value, int bucketIndex)
方法:将给定的哈希值、键、值和在数组中的下标添加到哈希表中。首先判断是否需要进行扩容,如果当前元素个数大于等于阈值且对应下标位置已经有元素存在,则调用resize(int newCapacity)
方法进行扩容。然后,重新计算哈希值和在数组中的下标,并调用createEntry(int hash, K key, V value, int bucketIndex)
方法创建新的节点并添加到链表中。- resize方法:
- newCapacity:新的容量大小。
- 首先获取旧的table数组,并获取旧的容量大小。
- 然后判断旧的容量是否等于最大容量,如果是,则将阈值设为最大整数值,表示无法再进行扩容。
- 创建新的table数组,长度为新的容量大小。
- 调用transfer方法将旧的table数组中的元素转移到新的table数组中。
- 将新的table数组赋值给table成员变量。
- 重新计算阈值。
10.transfer方法:
- newTable:新的table数组。
- rehash:是否需要重新计算hash值。
- 首先获取新的容量大小。
- 遍历旧的table数组中的每个元素。
- 对于每个元素,首先将其下一个元素赋值给next变量。
- 如果需要重新计算hash值,则将元素的hash值设为0或使用hash方法重新计算。
- 计算元素在新的table数组中的下标。
- 将元素的下一个元素设为新的table数组中该下标位置的元素。
- 将元素插入新的table数组中该下标位置。
- 将元素设为next变量的值,继续下一个元素的处理。
在resize方法中,首先判断旧的容量是否等于最大容量,如果是,则表示HashMap已经达到了最大容量,无法再进行扩容。然后创建新的table数组,长度为新的容量大小。接着调用transfer方法将旧的table数组中的元素转移到新的table数组中,同时根据需要重新计算hash值。最后重新计算阈值。
在transfer方法中,首先获取新的容量大小。然后遍历旧的table数组中的每个元素,对于每个元素,首先将其下一个元素赋值给next变量。如果需要重新计算hash值,则将元素的hash值设为0或使用hash方法重新计算。接着计算元素在新的table数组中的下标,并将元素插入新的table数组中该下标位置。最后将元素设为next变量的值,继续下一个元素的处理。这样就完成了
11.createEntry(int hash, K key, V value, int bucketIndex)方法:创建一个新的节点,并将其添加到链表的头部。首先,获取链表头部的节点
e。然后,创建一个新的节点,并将其赋值给链表头部。最后,增加元素个数
size。//头插法
- 节点类
Entry
:表示哈希表中的映射关系。每个节点包含键key
、值value
、下一个节点的引用next
和键的哈希值hash
。节点通过链表形式连接在一起,用于解决哈希冲突。
HashMap<Student, String> map = new HashMap<>();
map.put(new Student("小梦", '男', 21, "2301", "001"), "唱跳);
map.put(new Student("兴旺", '男', 23, "2301", "002"), "跑步");
map.put(new Student("喜儿", '男', 22, "2301", "003"), "听歌");
map.put(new Student("喜儿", '男', 22, "2301", "003"), "美食");
map.put(null, "玩游戏");
map.put(null, "写代码");
创建了一个
HashMap
对象map
,键的类型是Student
,值的类型是String
。然后,使用put
方法向map
中添加了五个键值对。
- 第一个键值对的键是一个
Student
对象,值是字符串"品茗"。- 第二个键值对的键是一个不同的
Student
对象,值是字符串"打篮球"。- 第三个键值对的键是又一个不同的
Student
对象,值是字符串"听歌"。- 第四个键值对的键是与第三个键值对的键相同的
Student
对象,值是字符串"美食"。这将会替换之前的值。- 第五个键值对的键是
null
,值是字符串"玩游戏"。- 第六个键值对的键是
null
,值是字符串"写代码"。这将会替换之前的值。请注意,
HashMap
允许键为null
,并且在这种情况下,null
键的值可以被替换。
存储内存图
学习HashMap的过程:
1. 创建对象的过程(注意:属性的初始化)
2. 添加数据的过程(注意:添加的步骤、hash碰撞)
hash碰撞
当两个key通过hashCod计算相同时(其实hashCode是随机产生的,是有可能hashCode相同),则发生了hash冲突,开放定址法、再哈希法、链地址法、建立公共溢出区
解决hash碰撞
用链表。当发生hash冲突时,则将存放在数组中的Entry设置为新值的next,说白就是比如A和B都hash后都映射到下标i中,之前已经有A了,当map.put(B)时,将B放到下标i中,A则为B的next,所以新值存放在数组中,旧值在新值的链表上
3.扩容的过程(注意:hash回环)
Hash回环
(也称Hash死循环)出现原因:多线程下,线程1不断添加新的数据导致HashMap扩容(数据的迁移) 线程2:不断地遍历,因此在多线程下要避免这个问题。
注意:
1、JDK1.7版本,HashMap的数据结构是什么? 答:单向链表+ 数组 2、HashMap中数组的默认初始化容量是多少? 答初始容量为(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR)为16,必须是2的幂。 3、什么叫作Hash桶? 答:数组中的单向链表 4、HashMap的数组长度为什么必须是2的幂? 答:计算元素存在数组中下标的算法:hash值&数组长度-1,如果数组长度不是2的幂,减1过后而精致的某一位可能出现0,导致数组某个位置永远存不到数据 5、HashMap的默认负载因子是多少,作用是什么? 答:负载因子:0.75 作用:数组长度-负载因子=阈值(扩容条件) 6、HashMap的默认负载因子(loadFactor)为什么是 0.75? 答:取得了时间与空间的平衡。 假设负载因子过大,导入数组装满后才扩容,牺牲时间,利用空间。 假设负载因子过小,导致数组装载较少内容就扩容,牺牲空间,利用时间 7、HashMap数组最大长度是多少?为什么是这么多? 答:最大长度为:1<<30,因为数组长度必须是2的幂并且HashMap数组最大长度的变量为int类型为了不超出界限,所以1<<30 8、什么叫作Hash碰撞? 答:两个对象的hash值一样,导致在数组中的下标一样 9、HashMap何时扩容? 元素个数>=阈值,并且存入数据的位置不等于null 10、HashMap的扩容机制是什么? 答:是原来的2倍 11、HashMap存入null键的位置? 答:hash数组下标为0的位置。 12、什么叫作hash回环? 答:Hash回环(也称Hash死循环)出现原因:多线程下,线程1不断添加新的数据导致HashMap扩容(数据的迁移) 线程2:不断地遍历,因此在多线程下要避免这个问题。 13、JDK1.7版本与JDK1.8版本的HashMap的区别? 答:JDK1.7:数组+链表(头插法),通过散列算法获取hash值 JKD1.8:数组+链表+红黑树(尾插法),通过低16位^高16位让hash值更加散列 14、JDK1.8版本HashMap为什么添加红黑树的数据结构? 答:因为链表查询慢,红黑树查询快 15、JDK1.8版本什么时候由 数组+链表变成数组+红黑树 答:当链表长度>8的时候并且数组长度>64,从数组+链表变成数组+红黑树 16、JDK1.8版本为什么链表长度大于8时,变成数组+链表+红黑树 答:因为泊松分布(统计概率学),当红黑树里的数据小于6时,又会将数组+红黑树变成数组+链表