上文详解HashMap源码解析(上)介绍了HashMap整体介绍了一下数据结构,主要属性字段,获取数组的索引下标,以及几个构造方法。本文重点讲解元素的添加查找扩容等主要方法。

添加元素

put(K key, V value)

public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

首先算出key的哈希码,调用hash方法,获取到hash值。

  • 调用putVal()
    /**
* @param hash hash for key hash 值
* @param key the key key 值
* @param value the value to put value 值
* @param onlyIfAbsent if true, don't change existing value 只有不存在,才不改变他的值
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none 返回上一个值,如果不存在返回null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 声明一个node数组 tab,node 节点
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果 table 为 null 或者 tab的长度为 0 ,|| 两边都要做一下判断,table 为空,或者table的长度为0
if ((tab = table) == null || (n = tab.length) == 0)
// table 初始化
n = (tab = resize()).length;
// 不存在,直接新建一个Node节点
if ((p = tab[i = (n - 1) & hash]) == null)
// 新建节点
tab[i] = newNode(hash, key, value, null);
else {
// 存在节点
Node<K,V> e; K k;
// hash值 和 p 节点的hash值一致,(键值的地址)一致或者(键的值)一致,直接替换
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 节点是红黑树
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,因为从零开始所以要减一
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 转成红黑树
treeifyBin(tab, hash);
break;
}
// hash一致 或者 值一致
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// e不为空,直接替换赋值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
// 原来的值为空,赋值
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
  • 首先判断哈希数组table是否为null,如果为null,就扩容。
  • (n - 1) & hash对应的下标是否存在节点。
    • 不存在节点,就创建新的节点并赋值。
    • 存在节点
      • 节点key值是否相等,相等就替换 value
      • 是否为红黑树,添加数据到红黑树中。
      • 上面都不符合,就是普通链表,遍历链表,如果链表存在相同key就替换,否则在链表最后添加数据。

流程图:

putAll(Map<? extends K, ? extends V> m)

putAll 是将集合元素全部添加到HashMap中,putAll调用了putMapEntries方法,putMapEntries先判断是否需要扩容,然后遍历元素,调用putVal添加元素,下面是添加元素代码:

for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}

获取数据

get(Object key)

通过key找到哈希表的中Node节点的value值。

// 返回map映射对应的value值
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

首先使用hash方法算出哈希值,然后再调用getNode()获取数据:

final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 判断tab有数据,并且对应下标存在数据
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// hash相等以及key相等(key地址相等或者key的值相等),找的就是第一个元素
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 遍历链表
if ((e = first.next) != null) {
// 红黑树找到当前key所在的节点位置
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 普通链表,往后遍历,直到找到数据或者遍历到链表末尾为止
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
  • 判断哈希数组是否不为null并且数组下标(n - 1) & hash处不为null,如果都有值,就查询首节点first,否则返回null
  • 找到首节点,匹配上相等的hashkey,返回首节点。
  • 链表有多个元素,是否为红黑树
    • 是红黑树,在红黑树查找
    • 不是红黑树,就遍历普通链表,直到匹配到相同的hashkey值。

流程图:

resize 扩容

当哈希数组为null,或元素个数超过了阈值,就调用resize扩容方法:

final Node<K,V>[] resize() {
// 记录原数组
Node<K,V>[] oldTab = table;
// 原数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 原阈值(数组长度达到阈值)
int oldThr = threshold;
// 新容量,新阈值
int newCap, newThr = 0;
if (oldCap > 0) {
// 数组长度大于或者等于MAXIMUM_CAPACITY(1>>30)不做扩容操作。
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 扩容后长度小于MAXIMUM_CAPACITY(1>>30)并且数组原来长度大于16
// 阈值和新容量都翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 阈值大于零,旧阈值替换成新容量
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// oldCap 和 oldThr 都小于等于0,说明是调用无参构造方法,赋值默认容量16,默认阈值12。
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
// 新阈值为零,计算阈值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 新建Node数组。调用无参构造方法,并不会创建数组,在第一次调用put方法,才会调用resize方法,才会创建数组,延迟加载,提高效率。
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 原来的数组不为空,把原来的数组的元素重新分配到新的数组中
// 如果是第一次调用resize方法,就不需要重新分配数组。
if (oldTab != null) {
// 旧数组遍历
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 存在下标下的第一个元素
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 当前元素下一个元素为空,说明此处只有一个元素,直接使用元素的hash值和新数组的容量取模,获得新下标的位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 红黑树,拆分红黑树,必要时可能退化为链表
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 长度大于1的普通链表
else { // preserve order
// loHead、loTail分别代表旧位置的头尾节点
Node<K,V> loHead = null, loTail = null;
// hiHead、hiTail分别代表新位置的头尾节点
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 遍历链表
do {
next = e.next;
// & 与运算,两个都会1,结果才为1
// 元素的hash值和oldCap与运算为0,原位置不变
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 移动到原来位置 + oldCap
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
  • 原容量是否为空

    • 不为空,是否大于最大容量

      • 大于最大容量,不做扩容
      • 小于最大容量,并且大于默认容量16。阈值和容量都翻倍。
    • 为空,原阈值大于零, 就阈值赋值给新容量。
  • 原容量和原阈值都小于等于零,赋值默认容量16和默认阈值12。
  • 做完阈值和容量的赋值之后,遍历数组。
  • 有值,是否只有一个元素,如果是就放入新数组n-1&hash下标处。
  • 如果是红黑树就拆分红黑树。
  • 上面两个都不符合就是普通链表。
  • 遍历链表,如果hash&数组原长度为0
    • 放在数组原下标处。
    • 不为零,放在原位置+原数组长度处。

流程图:

总结

本文主要讲解了元素的添加查找扩容等主要方法,其中添加查询都需要先获取数组的下标,然后进行对应的操作。

put添加

  • 首次添加数据需要对数组进行扩容。
  • 对应下标是否有值
    • 没有值,直接赋值
    • 有值
      • key一致,替换value值。
      • key不一致
        • 是红黑树,在红黑树添加数据。
        • 不是红黑树,就是链表,遍历链表,存在相同节点key,替换。否者添加在链表的尾部。

get查询

  • 下标是否有值

    • 没有值,返回null
    • 有值

      *hashkey相等的话,返回节点。

      • 是否是多链表。

        • 不是,返回null
        • 是的话,是否是红黑树。
          • 红黑树,在红黑树中查找
          • 否则就是普通链表,遍历链表知道匹配到相同的hashkey

resize 扩容

  • 容量大于零

    • 大于最大容量值,不再扩容。
    • 介于最大和默认容量之间,阈值和容量都翻倍。
  • 初始化的时候,设置默认容量和默认阈值。
  • 遍历原数组
  • 节点有值,并且只有一个值,赋值给新数组n-1&hash处。
  • 如果是红黑树,就拆分红黑树。
  • 以上都不符合,就是普通链表,遍历链表。因为数组长度都是2的幂次方,扩容后元素的位置*要么是在原位置,要么是在原位置再移动2次幂的位置
    • hash&与运算原数组长度,等于0,存在原来的位置。
    • 不等于0,就存放下标原来位置+原数组长度位置处。

最新文章

  1. innerHTML和innerText的区别
  2. H5案例分享:移动端滑屏 touch事件
  3. [MAC]2015款MACBOOK使用BOOTCAMP安装WIN8.1+多分区
  4. poj 1701【数学几何】
  5. 最小生成树之Kruskal
  6. memcmp()直接比较两个数组的大小
  7. 「OC」 基本语法
  8. Android百度地图定位
  9. Spring中一个类的注入和引用是不一样的
  10. 201521123057 《Java程序设计》第2周学习总结
  11. Yii2 灵活加载js、css
  12. 查询运营商的ip段
  13. NSCTF web200
  14. day44 mysql高级部分内容
  15. 使用双引擎,让kbmmw 的客户端访问更方便
  16. sh命令
  17. 一款好用的轮播插件swiper,适用于移动端和web
  18. PyQt5--QComboBox
  19. 应用ArcGIS Server JavaScript API实现地图卷帘效果实现
  20. 第一个struts程序的配置过程

热门文章

  1. 简单了解AndroidManifest.xml文件
  2. Bugku CTF练习题---分析---flag被盗
  3. 从零开始搭建高可用的k8s集群
  4. 手机USB共享网络是个啥
  5. VS Code 真的会一统江湖吗?
  6. gcc和g++是什么,有什么区别?
  7. 万字长文详解HBase读写性能优化
  8. 2021夏季学期华清大学EE数算OJ1:算数问题
  9. 【Java8新特性】Optional 类
  10. 学习Java的第十七天——大数字运算