HashMap知识点(一)

HashMap知识点(一)

一、HashMap扩容机制原理

1、JDK1.7版本扩容机制

生成一个新的数组,将原数组的元素全都转移到新的数组上

resize方法源码

/**
* 分析:resize(2 * table.length)
* 作用:当容量不足时(容量 > 阈值),则扩容(扩到2倍)
*/
void resize(int newCapacity) {
// 1. 保存旧数组(old table)
Entry[] oldTable = table;
// 2. 保存旧容量(old capacity ),即数组长度
int oldCapacity = oldTable.length;
// 3. 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出
if (oldCapacity == MAXIMUM_CAPACITY) {
// 修改扩容阀值
threshold = Integer.MAX_VALUE;
return;
}
// 4. 根据新容量(2倍容量)新建1个数组,即新table
Entry[] newTable = new Entry[newCapacity];
// 5. 将旧数组上的数据(键值对)转移到新table中,从而完成扩容。initHashSeedAsNeeded(newCapacity)这个方法用来根据新的数组长度来重新初始化Hash种子
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// 6. 新数组table引用到HashMap的table属性上
table = newTable;
// 7. 重新设置阈值,如果阈值超过了HashMap最大容量大小,则直接将阈值设置为 MAXIMUM_CAPACITY + 1
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

initHashSeedAsNeeded()方法源码

这个方法在JDK1.7中inflateTable()初始化哈希表方法和resize()扩容方法中都有出现。这个方法作用是初始化Hash种子。在JDK1.7中计算hash值的方法需要使用Hash种子来参与运算,进而提高计算出来的hash值的散列性,最大限度减少哈希冲突。下面就来简单讲一下这个方法:

* 这个方法用来根据新的数组长度来重新初始化Hash种子,好的Hash种子能提高计算Hash时结果的散列性,最大限度减少哈希冲突。
* @param capacity 根据传入的容量大小来进行重新初始化Hash种子
* @return 返回true说明已经根据传入的容量大小重新初始化了Hash种子,此时以前根据旧的Hash种子计算出来的Hash值就需要进行rehash了。
* 返回false说明并没有根据传入的容量大小进行重新初始化Hash种子
*/
final boolean initHashSeedAsNeeded(int capacity) {
// 首先会判断hashSeed是否不等于0,因为hashSeed一开始是0,所以此处是false
boolean currentAltHashing = hashSeed != 0;
// 这行代码是判断vm是否启动 且 容量到达一个值ALTERNATIVE_HASHING_THRESHOLD,这个值是可以自己去设定,不设定的话是默认的Integer.MaxValue 。 假设我们初始化容量capacity = 16,设置ALTERNATIVE_HASHING_THRESHOLD值为 3,那么这行代码会为true
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
//亦或 ^ 的意思是 不相同则返回true,此时switching=true,那么hashSeed就会重新去计算hash种子,以便计算hash时增加散列性,
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
// 重新设置了Hash种子
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
final int hash(Object k) {
// 设置了哈希种子
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
// Hash种子参与到了key的Hash值计算当中
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

addEntry方法

void addEntry(int hash,k key,v vaule,int bucketIndex){
//threshole为扩容阈值,及最大值*加载因子
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);
}

transfer方法

void transfer(Entry[] newTable,boolean rehash){
int newCapacity = newTable.length;
for(Entry<K,V> e : table){
while(nul != 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;
}
}
}

扩容后原来的元素只会在新数组的原位置或原位置+原数组大小处

new =old|new = old + 16

2、JDK1.8版本扩容机制

方法执行流程

**resize()****方法执行流程**

java8 resize方法源码

/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
* 初始化或把table容量翻倍。如果table是空,则根据threshold属性的值去初始化HashMap的容
* 量。如果不为空,则进行扩容,因为我们使用2的次幂来给HashMap进行扩容,所以每个桶里的元素
* 必须保持在原来的位置或在新的table中以2的次幂作为偏移量进行移动
* @return 返回Node<K, V>数组
*/
final Node<K,V>[] resize() {
// 创建一个临时变量,用来存储当前的table
Node<K,V>[] oldTab = table;
// 获取原来的table的长度(大小),判断当前的table是否为空,如果为空,则把0赋值给新定义的oldCap,否则以table的长度作为oldCap的大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 创建临时变量用来存储旧的阈值,把旧table的阈值赋值给oldThr变量
int oldThr = threshold;
// 定义变量newCap和newThr来存放新的table的容量和阈值,默认都是0
int newCap, newThr = 0;
// 判断旧容量是否大于0
if (oldCap > 0) {
// 判断旧容量是否大于等于 允许的最大值,2^30
if (oldCap >= MAXIMUM_CAPACITY) {
// 以int的最大值作为原来HashMap的阈值,这样永远达不到阈值就不会扩容了
threshold = Integer.MAX_VALUE;
// 因为旧容量已经达到了最大的HashMap容量,不可以再扩容了,将阈值变成最大值之后,将原table返回
return oldTab;
}
// 如果原table容量不超过HashMap的最大容量,将原容量*2 赋值给变量newCap,如果newCap不大于HashMap的最大容量,并且原容量大于HashMap的默认容量
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 将newThr的值设置为原HashMap的阈值*2
newThr = oldThr << 1; // double threshold
}
// 如果原容量不大于0,即原table为null,则判断旧阈值是否大于0
else if (oldThr > 0) // 如果原table为Null且原阈值大于0,说明当前是使用了构造方法指定了容量大小,只是声明了HashMap但是还没有真正的初始化HashMap(创建table数组),只有在向里面插入数据才会触发扩容操作进而进行初始化
// 将原阈值作为容量赋值给newCap当做newCap的值。由之前的源码分析可知,此时原阈值存储的大小就是调用构造函数时指定的容量大小,所以直接将原阈值赋值给新容量
newCap = oldThr;
// 如果原容量不大于0,并且原阈值也不大于0。这种情况说明调用的是无参构造方法,还没有真正初始化HashMap,只有put()数据的时候才会触发扩容操作进而进行初始化
else { // zero initial threshold signifies using defaults
// 则以默认容量作为newCap的值
newCap = DEFAULT_INITIAL_CAPACITY;
// 以初始容量*默认负载因子的结果作为newThr值
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 经过上面的处理过程,如果newThr值为0,说明上面是进入到了原容量不大于0,旧阈值大于0的判断分支。需要单独给newThr进行赋值
if (newThr == 0) {
// 临时阈值 = 新容量 * 负载因子
float ft = (float)newCap * loadFactor;
// 设置新的阈值 保证新容量小于最大总量 阈值要小于最大容量,否则阈值就设置为int最大值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 将新的阈值newThr赋值给threshold,为新初始化的HashMap来使用
threshold = newThr;
// 初始化一个新的容量大小为newCap的Node数组
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 将新创建的数组赋值给table,完成扩容后的新数组创建
table = newTab;
// 如果旧table不为null,说明旧HashMap中有值
if (oldTab != null) {
// 如果原来的HashMap中有值,则遍历oldTab,取出每一个键值对,存入到新table
for (int j = 0; j < oldCap; ++j) {
// 创建一个临时变量e用来指向oldTab中的第j个键值对,
Node<K,V> e;
// 将oldTab[j]赋值给e并且判断原来table数组中第j个位置是否不为空
if ((e = oldTab[j]) != null) {
// 如果不为空,则将oldTab[j]置为null,释放内存,方便gc
oldTab[j] = null;
// 如果e.next = null,说明该位置的数组桶上没有连着额外的数组
if (e.next == null)
// 此时以e.hash&(newCap-1)的结果作为e在newTab中的位置,将e直接放置在新数组的新位置即可
newTab[e.hash & (newCap - 1)] = e;
// 否则说明e的后面连接着链表或者红黑树,判断e的类型是TreeNode还是Node,即链表和红黑树判断
else if (e instanceof TreeNode)
// 如果是红黑树,则进行红黑树的处理。将Node类型的e强制转为TreeNode,之所以能转换是因为TreeNode 是Node的子类
// 拆分树,具体源码解析会在后面的TreeNode章节中讲解
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 当前节不是红黑树,不是null,并且还有下一个元素。那么此时为链表
else { // preserve order
/*
这里定义了五个Node变量,其中lo和hi是,lower和higher的缩写,也就是高位和低位,
因为我们知道HashMap扩容时,容量会扩到原容量的2倍,
也就是放在链表中的Node的位置可能保持不变或位置变成 原位置+oldCap,在原位置基础上又加了一个数,位置变高了,
这里的高低位就是这个意思,低位指向的是保持原位置不变的节点,高位指向的是需要更新位置的节点
*/
// Head指向的是链表的头节点,Tail指向的是链表的尾节点
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
// 指向当前遍历到的节点的下一个节点
Node<K,V> next;
// 循环遍历链表中的Node
do {
next = e.next;
/*
如果e.hash & oldCap == 0,注意这里是oldCap,而不是oldCap-1。
我们知道oldCap是2的次幂,也就是1、2、4、8、16...转化为二进制之后,
都是最高位为1,其它位为0。所以oldCap & e.hash 也是只有e.hash值在oldCap二进制不为0的位对应的位也不为0时,
才会得到一个不为0的结果。举个例子,我们知道10010 和00010 与1111的&运算结果都是 0010 ,
但是110010和010010与10000的运算结果是不一样的,所以HashMap就是利用这一点,
来判断当前在链表中的数据,在扩容时位置是保持不变还是位置移动oldCap。
*/
// 如果结果为0,即位置保持不变
if ((e.hash & oldCap) == 0) {
// 如果是第一次遍历
if (loTail == null)
// 让loHead = e,设置头节点
loHead = e;
else
// 否则,让loTail的next = e
loTail.next = e;
// 最后让loTail = e
loTail = e;
}
/*
其实if 和else 中做的事情是一样的,本质上就是将不需要更新位置的节点加入到loHead为头节点的低位链表中,将需要更新位置的节点加入到hiHead为头结点的高位链表中。
我们看到有loHead和loTail两个Node,loHead为头节点,然后loTail是尾节点,在遍历的时候用来维护loHead,即每次循环,
更新loHead的next。我们来举个例子,比如原来的链表是A->B->C->D->E。
我们这里把->假设成next关系,这五个Node中,只有C的hash & oldCap != 0 ,
然后这个代码执行过程就是:
第一次循环: 先拿到A,把A赋给loHead,然后loTail也是A
第二次循环: 此时e的为B,而且loTail != null,也就是进入上面的else分支,把loTail.next =
B,此时loTail中即A->B,同样反应在loHead中也是A->B,然后把loTail = B
第三次循环: 此时e = C,由于C不满足 (e.hash & oldCap) == 0,进入到了我们下面的else分支,其
实做的事情和当前分支的意思一样,只不过维护的是hiHead和hiTail。
第四次循环: 此时e的为D,loTail != null,进入上面的else分支,把loTail.next =
D,此时loTail中即B->D,同样反应在loHead中也是A->B->D,然后把loTail = D
*/
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 遍历结束,即把table[j]中所有的Node处理完
// 如果loTail不为空,也保证了loHead不为空
if (loTail != null) {
// 此时把loTail的next置空,将低位链表构造完成
loTail.next = null;
// 把loHead放在newTab数组的第j个位置上,也就是这些节点保持在数组中的原位置不变
newTab[j] = loHead;
}
// 同理,只不过hiHead中节点放的位置是j+oldCap
if (hiTail != null) {
hiTail.next = null;
// hiHead链表中的节点都是需要更新位置的节点
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 最后返回newTab
return newTab;
}

3、JDK1.7和JDK1.8的区别

img

二、HashMap和HashTable的区别?底层实现是什么?

1、HashMap和HashTable的区别

(1)最基本的区别HashTable是线程安全的,HashMap是线程不安全的,因为HashTable的每一个方法都使用synchronized关键字修饰。HashTable效率低下,现已不常使用,多使用CurrentHashMap.

(2)HashMap允许key和value为null,而HashTable不允许

2、底层实现是什么

jdk8开始链表高度到8、数组长度超过64,链表转换为红黑树,元素以内部类Node结点存在

  • 计算key的hash值,二次hash然后对数组长度取模,对应到数组下标

  • 如果没有产生hash冲突(下标位置没有元素),则直接创建Node数组

  • 如果产生hash冲突,先进行equal比较,相同则取代元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表

  • key为null,存在下标0的位置

三 、HashMap里put方法的实现流程

1、put方法的作用和执行流程

HashMap 只提供了 put 用于添加元素,putval也是使用的默认修饰符,因此只能被本类或者该包下的类访问到,所以putVal 方法只是给 put 方法调用的一个方法,并没有提供给用户使用。

对 putVal 方法添加元素的分析如下:

1、如果定位到的数组位置没有元素,就直接插入。
2、如果定位到的数组位置有元素就和要插入的 key 比较,如果 key 相同就直接覆盖,如果 key 不相同,就判断 p 是否是一个树节点,如果是就调用e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。

img

2、put()和putVal()源码

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods.
* 实现了map的put和相关方法
* @param hash key的hash值(key的hash高16位+高16位与低16位的异或运算)
* @param key 键
* @param value 值
* @param onlyIfAbsent onlyIfAbsent为true的时候不要修改已经存在的值,如果onlyIfAbsent为false,当插入的元素已经在HashMap中已经拥有了与其key值和hash值相同的元素,仍然需要把新插入的value值覆盖到旧value上。如果nlyIfAbsent为true,则不需要修改
* @param evict evict如果为false表示构造函数调用
* @return 返回旧的value值(在数组桶或链表或红黑树中找到存在与插入元素key值和hash值相等的元素,就返回这个旧元素的value值),如果没有发现相同key和hash的元素则返回null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// tab用来临时存放数组table引用 p用来临时存放数组table桶中的bin
// n存放HashMap容量大小 i存放当前put进HashMap的元素在数组中的位置下标
Node<K,V>[] tab; Node<K,V> p; int n, i;
// table未初始化或者长度为0,进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 桶中已经存在元素
else {
// e记录当前节点 k记录key值
Node<K,V> e; K k;
// 比较桶中第一个元素(数组中的结点)的hash值相等,key相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 将第一个元素赋值给e,用e来记录。直接将插入的新元素覆盖旧元素
e = p;
// hash值不相等,即key不相等并且该节点为红黑树结点,将元素插入红黑树
else if (p instanceof TreeNode)
// 放入树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 为链表结点
else {
// 在链表最末插入结点(尾插法)
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部
if ((e = p.next) == null) {
// 在尾部插入新结点
p.next = newNode(hash, key, value, null);
// 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法
// 这个treeifyBin()方法会根据 HashMap 数组情况来决定是否转换为红黑树。
// 只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少执行效率。否则,就是只是对数组扩容。
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 树化操作
treeifyBin(tab, hash);
// 跳出循环 此时e=null,表示没有在链表中找到与插入元素key和hash值相同的节点
break;
}
// 判断链表中结点的key值和Hash值与插入的元素的key值和Hash值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 若相等,则不用将其插入了,直接跳出循环
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 当e!=null时,表示在数组桶或链表或红黑树中存在key值、hash值与插入元素相等的结点。此时就直接用原有的节点就可以了,不用插入新的元素了。此时e就代表原本就存在于HashMap中的元素
if (e != null) {
// 记录e的value,也就是旧value值
V oldValue = e.value;
// onlyIfAbsent为false或者旧值为null,则需要用新的value值对旧value值进行覆盖
if (!onlyIfAbsent || oldValue == null)
//用新值替换旧值
e.value = value;
// 替换旧值时会调用的方法(默认实现为空)
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 结构性修改,记录HashMap被修改的次数,主要用于多线程并发时候
++modCount;
// 实际大小大于阈值则扩容 ++size只有在插入新元素才会执行,如果发现HashMap中已经存在了相同key和hash的元素,就不会插入新的元素,在上面就已经执行return了,也就不会改变size大小
if (++size > threshold)
resize();
// 插入成功时会调用的方法(默认实现为空)
afterNodeInsertion(evict);
// 没有找到原有相同key和hash的元素,则直接返回Null
return null;
}

3、对比JDK1.7的put()方法源码

对于JDK1.7的 put 方法的分析如下:

1、如果定位到的数组位置没有元素 就直接插入。
2、如果定位到的数组位置有元素,遍历以这个元素为头结点的链表,依次和链表上的 key 比较,如果存在key 相同的节点就直接覆盖,没有相同的节点就采用头插法将元素插入链表。与JDK1.8链表插入元素的不同点就在于1.8是尾插法,1.7是头插法
img

首先贴一下JDK1.7中HashMap成员属性与1.8相比不同的两个,在put()源码中会出现

//HashMap内部的存储结构是一个数组,此处数组为空,即没有初始化之前的状态
static final Entry<?,?>[] EMPTY_TABLE = {};
//空的存储实体 table是真正存储元素的数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

put()方法源码:

/**
* 将“key-value”添加到HashMap中
* @return 如果插入的key在HashMap中已存在,将新插入的value替换旧value,并且返回旧value
* 如何插入的key在HashMap中不存在,则返回Null
*/
public V put(K key, V value) {
// 1. 若 哈希表未初始化(即 table为空)
// 则使用构造函数进行初始化 数组table
if (table == EMPTY_TABLE) {
// 分配数组空间
// 入参为threshold,此时threshold为initialCapacity initialCapacity可以是构造方法中传入的大小,如果构造方法没有指定HashMap容量大小,则使用默认值1<<4(=16)
inflateTable(threshold);
}
// 2. 判断key是否为空值null
// 2.1 若key == null,则将该键-值 存放到数组table 中的第1个位置,即table [0]
// (本质:key = Null时,hash值 = 0,故存放到table[0]中)
// 该位置永远只有1个value,新传进来的value会覆盖旧的value
if (key == null)
return putForNullKey(value);
// 2.2 若 key ≠ null,则计算存放数组 table 中的位置(下标、索引)
// a. 根据键值key计算hash值
int hash = hash(key);
// b. 根据hash值 最终获得 key对应存放的数组Table中位置
int i = indexFor(hash, table.length);
// 3. 判断该key对应的值是否已存在(通过遍历 以该数组元素为头结点的链表 逐个判断)
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
// 3.1 若该key已存在(即 key-value已存在 ),则用新value替换旧value,并返回旧value
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
// 调用value的回调函数,该函数为空实现
e.recordAccess(this);
return oldValue;
}
}
// 结构性修改,记录HashMap被修改的次数。保证并发访问时,若HashMap内部结构发生变化,快速响应失败
modCount++;
// 3.2 若 该key不存在,则将“key-value”添加到table中
addEntry(hash, key, value, i);
return null;
}

1)、初始化哈希表

真正初始化哈希表(初始化存储数组table)是在第1次添加键值对时,即第1次调用put()时,而不是在构造函数中。

inflateTable()方法用于初始化HashMap,即初始化数组(table)、扩容阈值(threshold)。

​ ****inflateTable的源码如下:

/**
* 初始化hash表
* @param toSize 指定HashMap容量大小
*/
private void inflateTable(int toSize) {
// 1. capacity必须是2的次幂,将传入的容量大小toSize转化为:大于传入容量大小toSize的最小的2的次幂
// 即如果传入的是容量大小是19,那么转化后,初始化容量大小为32(即2的5次幂)
int capacity = roundUpToPowerOf2(toSize);
// 2. 重新计算阈值 threshold = 容量 * 加载因子
// 取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 3. 使用计算后的初始容量(已经是2的次幂) 为table分配空间,即初始化数组table(作为数组长度)
// 即 哈希表的容量大小 = 数组大小(长度)
table = new Entry[capacity];
// 选择合适的Hash因子(即Hash种子),好的Hash种子能提高计算Hash时结果的散列性
initHashSeedAsNeeded(capacity);
}

inflateTable这个方法用于为主干数组table在内存中分配存储空间,通过roundUpToPowerOf2(toSize)可以确保capacity为大于或等于toSize的最接近toSize的二次幂,比如toSize=13,则capacity=16;to_size=16,capacity=16;to_size=17,capacity=32。其实现如下:

/**
* 找到大于传入容量大小的最小的2的次幂
*/
private static int roundUpToPowerOf2(int number) {
//若 容量超过了最大值,初始化容量设置为最大值 ;否则,设置为:大于传入容量大小的最小的2的次幂
return number >= MAXIMUM_CAPACITY ?
MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

roundUpToPowerOf2中的这段处理使得数组长度一定为2的次幂,Integer.highestOneBit是用来获取最左边的bit(其他bit位为0)所代表的数值。

2)、当 key ==null时,将该 key-value 的存储位置规定为数组table 中的第1个位置,即table [0]

/**
* 将key为null的value值放入table[0]上
*/
private V putForNullKey(V value) {
// 遍历以table[0]为首的链表,寻找是否存在key==null 对应的键值对
// 1. 若有:则用新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;
}
}
}

从此处可以看出:

  • HashMap的键key 可为null(区别于 HashTable的key 不可为null)
  • HashMap的键key 可为null且只能为1个,但值value可为null且为多个

3)、当key≠null的时候,计算key的Hash并根据Hash值计算对应在able中的下标

hash()方法:

/**
* 源码分析1:hash(key)
* 该函数在JDK 1.7 和 1.8 中的实现不同,但原理一样 = 扰动函数 = 使得根据key生成的哈希码(hash值)分布更加均匀、更具备随机性,避免出现hash值冲突(即指不同key但生成同1个hash值)
* JDK 1.7 做了9次扰动处理 = 4次位运算 + 5次异或运算
* JDK 1.8 简化了扰动函数 = 只做了2次扰动 = 1次位运算 + 1次异或运算
*/
// JDK 1.7实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)
static final int hash(int h) {
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

通过hash函数得到散列值后,再通过indexFor进一步处理来获取实际的存储位置,其实现如下:

/**
* 函数源码分析2:indexFor(hash, table.length)
* JDK 1.8中实际上无该函数,但原理相同,即具备类似作用的函数
*/
static int indexFor(int h, int length) {
// 将对哈希码扰动处理后的结果 与运算(&) (数组长度-1),最终得到存储在数组table的位置(即数组下标、索引)
return h & (length-1);
}

4)、当key≠null时,得到存储的下标位置后,我们就可以将元素放入HashMap中

先判断链表中是否已经存在与要插入元素的key相同的元素,如果有,直接用要插入的新value覆盖旧value。如果没有,则调用addEntry()方法将元素插入:

/**
* 添加链表元素
* 作用:添加键值对(Entry )到 HashMap中
* @param bucketIndex 元素要插入到数组table的索引位置(下标)
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
// 1. 插入前,先判断容量是否足够
// 1.1 若不足够,则进行扩容(2倍)、重新计算Hash值、重新计算存储数组下标
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length); // a. 扩容2倍
hash = (null != key) ? hash(key) : 0; // b. 重新计算该Key对应的hash值
bucketIndex = indexFor(hash, table.length); // c. 重新计算该Key对应的hash值的存储数组下标位置
}
// 1.2 若容量足够,则创建1个新的数组元素(Entry) 并放入到数组中
createEntry(hash, key, value, bucketIndex);
}
/**
* 创建元素,并将新元素添加到HashMap中
* 作用: 若容量足够,则创建1个新的数组元素(Entry) 并放入到数组中
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
// 1. 把table中该位置原来的Entry保存
Entry<K,V> e = table[bucketIndex];
// 2. 使用头插法讲元素插入到链表中,新元素成为链表头节点,新元素的next节点为原链表头节点。这保证了新插入的元素总是在链表的头
table[bucketIndex] = new Entry<>(hash, key, value, e);
// 3. 哈希表的键值对数量计数增加
size++;
}

4、JDk1.7和JDK1.8的区别

img

posted @   RitoQ  阅读(113)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
点击右上角即可分享
微信分享提示