IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> HashMap源码分析 -> 正文阅读

[数据结构与算法]HashMap源码分析

HashMap源码分析

Hash算法

Hash也被称为散列、哈希。他们的基本原理都是**把任意长度的输入,通过Hash算法变成固定长度的输出。**这个映射的规则就是对应的Hash算法,而原始数据映射之后的二进制串就是哈希值。

HashMap

继承体系

在这里插入图片描述

  • HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。

  • HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步。

  • HashMap 是无序的,即不会记录插入的顺序。

  • HashMap 继承于AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口。

  • HashMap 的 key 与 value 类型可以相同也可以不同,可以是字符串(String)类型的 key 和 value,也可以是整型(Integer)的 key 和字符串(String)类型的 value。

存储结构

HashMap采用了数组 + 链表 + 红黑树的结构进行存储元素,并且数组的一个元素被称为桶。

在添加元素时,会先计算哈希值,然后根据哈希值算出元素要存放的位置。在存放元素前,会先判断要存放的位置上是否有元素,如果没有元素,就直接将元素放置到此处;如果该位置上已经有元素了,就该用链表的方式存储,将元素放入链表的尾部。

当链表达到一定长度时(即长度为8时),并且整个数组的达到达到一定长度时(即大小为64时),就会将链表改为红黑树的方式进行存储,从而提高效率。

扩容机制:

  • HashMap桶(用于存放元素的数组)的初始大小默认为 16 (2的幂)
  • 如果桶位数(数组)小于64,每次扩容都是原来的2倍(容量大小都是2的幂),这是扩容后会重新计算桶内元素的哈希值并重新计算存放位置,这样就可以使得使用链表存放的方式减少或使链表变短,提升桶空间利用率的同时提升效率。
  • 当桶中某位的链表长度大于8并且桶位数大于64时,会对大于8的链表进行树化操作,即:将链表转换为红黑树
  • 当红黑树的结点小于6时,红黑树会重新转换为链表

源码分析

HashMap的基本属性与常量

/************常量************/

/**
 * 默认初始容量 - 必须是 2 的幂。
 * 默认设置为16
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
 * 最大容量,如果一个更高的值被任何一个带参数的构造函数隐式指定使用。
 * 必须是 2 <= 1<<30 的幂。
 * 默认设置为1073741824 即为2的30次幂。
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
 * 构造函数中未指定负载因子时使用的负载因子。
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 树化因子
 * 当一个桶中的元素个数大于等于8时进行树化
 */
static final int TREEIFY_THRESHOLD = 8;

/**
 * 取消阈值
 * 当红黑树中元素个数小于6时将红黑树转化为链表
 */
static final int UNTREEIFY_THRESHOLD = 6;

/**
 * 可对其进行树化的最小表容量
 * 超过64时,并且有链表的长度大于8,就将此链表树化
 */
static final int MIN_TREEIFY_CAPACITY = 64;

 /* ---------------- Fields -------------- */

 /**
  * 用于存放元素的表,也被称为桶
  * 在首次使用时初始化,长度始终是2的幂
  */
 transient Node<K,V>[] table;

 /**
  * 保存缓存的 entrySet()。
  * 请注意,AbstractMap 字段用于 keySet() 和 values()。
  */
 transient Set<Map.Entry<K,V>> entrySet;

 /**
  * 元素的数量
  */
 transient int size;

 /**
  * 修改次数,用于在迭代的时候执行快速失败策略
  */
 transient int modCount;

 /**
  * 当桶的使用数量达到多少时进行扩容,是一个动态计算的值
  * threshold = capacity * loadFactor
  */
 int threshold;

 /**
  * 哈希表的负载因子.
  */
 final float loadFactor;

总结:

(1)容量

容量为数组的长度,亦即桶的个数,默认为16,最大为2的30次方,当容量达到64时才可以树化

(2)装载因子

装载因子用来计算容量达到多少时才进行扩容,默认装载因子为0.75

(3)树化

树化,当容量达到64且链表的长度达到8时进行树化,当链表的长度小于6时反树化。

Node内部类

Node的实例表示一个单链表节点,用于存储key和value。

/**
 * 基本哈希 bin 节点,用于大多数条目。
 * (参见下面的 TreeNode 子类,以及 LinkedHashMap 中的 Entry 子类。)
 */
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash; // 存储哈希值
    final K key;
    V value;
    Node<K,V> next; // 存储下一个节点的索引

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

HashMap()构造方法

// 空参的构造方法,先不初始化table而是将负载因子赋予默认值DEFAULT_LOAD_FACTOR(0.75)
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

HashMap(int initialCapacity)构造方法

调用HashMap(int initialCapacity, float loadFactor)构造方法,传入默认装载因子。

public HashMap(int initialCapacity) {
    // DEFAULT_LOAD_FACTOR = 0.75
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

HashMap(int initialCapacity, float loadFactor)构造方法

判断传入的初始化容量是否和法同时计算扩容的门槛,扩容门槛为传入的初始容量往上取最近的2的n次方。

// initialCapacity:传入的初始化容量
// loadFactor:负载因子
public HashMap(int initialCapacity, float loadFactor) {
    // 检验初始化容量是否合法
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    // 如果initialCapacity大于MAXIMUM_CAPACITY(最大容量)就将其设置为MAXIMUM_CAPACITY
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 检验负载因子是否合法
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    // 计算扩容门槛
    this.threshold = tableSizeFor(initialCapacity);
}

/**
 * 返回给定目标容量的最近的 2 次方数。
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

put(K key, V value)方法

向表中添加元素的方法

public V put(K key, V value) {
    // 实际是putVal方法完成添加元素功能
    return putVal(hash(key), key, value, false, true);
}

/**
 * 实现 Map.put 和相关方法。
 *
 * @param hash 键的哈希
 * @param key 键
 * @param value 要插入的值
 * @param onlyIfAbsent 如果为真,则不要更改现有值
 * @param evict 如果为 false,则表处于创建模式。
 * @return 前一个值,如果没有,则为 null
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // tab: 引用当前哈希表
    // p: 用于引用哈希表中的某个元素
    // n: 记录哈希表数组的长度
    // i: 记录寻址
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果此时的哈希表是null或者大小是0, 则初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 这里有两部操作:
    // 1. (n - 1) & hash 计算出元素该放到哈希表中的什么位置
    // 2. 判断此位置上是否有元素
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 没有元素就调用newNode方法创建结点,将元素放到此位置上
        tab[i] = newNode(hash, key, value, null);
    else {
        // 计算出的位置上游元素的情况:

        // e: 在后面的遍历中不是null的情况下,表示找到了一个与当前要插入的key-value一致的key元素
        // k: 用于表示一个临时的key
        Node<K,V> e; K k;
        // 判断哈希表中此位置上的第一个元素与要插入的元素hash相同并且key的值相同,就保存到e中用于后续修改value值
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果当前位置上的结点是一个TreeNode节点(红黑树节点),就用putTreeVal方法插入元素
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            // 遍历链表,对每个元素进行判断
            for (int binCount = 0; ; ++binCount) {
               // 如果遍历到了链表的尾部,没有出现key相同的元素的情况,就向链表末尾加入元素
                if ((e = p.next) == null) {
                    // 先将p结点创建
                    p.next = newNode(hash, key, value, null);
                    // 此时需要进行判断,未插入p时的结点是否已经到达了8个
                    // 如果已经到达了8个,那么插入p结点链表长度就变成了9 > 8,就需要对链表进行树化操作
                    // 也就是插入新元素导致链表长度大于8时就树化
                    // 同时注意:因为binCount是从0开始的,所以需要用TREEIFY_THRESHOLD - 1,即8-1=7
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 当链表中的节点的hash值和key的值与待插入元素的hash值和key值分别相同时终止循环,后续进行修改value值操作
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                // 实际是p,e两个指针遍历链表
                p = e;
            }
        }

        // 如果找到了对应key的元素
        if (e != null) { // existing mapping for key
            // 记录旧的value值
            V oldValue = e.value;
            // 判断是否需要替换旧值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 访问到此节点时可以做一些事情,在LinkedHashMap中用到
            afterNodeAccess(e);
            return oldValue; // 返回旧的值
        }
    }
    // 这里表示没有找到相同key的元素
    // 修改次数 +1 (不包括key相同时值替换的操作)
    ++modCount;
    // 哈希表内元素数量+1, 并判断是否需要扩容
    if (++size > threshold)
        resize(); // 扩容操作
    // 在插入节点后可以做一些事情,在LinkedHashMap中用到
    afterNodeInsertion(evict);
    // 没有出现过替换旧值得操作,返回null
    return null;
}

总结:

  1. 实际实现功能的是putVal方法
  2. 计算插入元素的位置
  3. 计算出的位置上已经有元素了
    • 比较key是否相同
      • 相同则记录该元素,后续进行替换操作
    • 判断要插入位置上的节点是否已经是一个红黑树
      • 是红黑树就putTreeVal方法进行插入
    • 要插入元素的位置上是一个链表结构,需要依次比较链表中的元素
      • 如果走到了链表的尽头,先创建新的节点准备插入
        • 如果此时原来的链表长度已经到达8了,就将链表树化后,在插入新元素
        • 如果链表长度没有大于8,就插入新元素
      • 在链表中出现了相同的元素,就记录元素,后续进行替换操作
    • 判断是否需要替换旧元素
      • 如果是,就将旧的value值替换掉,并将旧的value值返回
  4. 对哈希表的修改次数(modCount)加一,但是不包括key相同时值替换的操作
  5. 将哈希表元素数量(size)加一,并判断是否超过threshold阈值
    • 超过阈值就进行扩容操作
  6. 如果没有出现过替换旧值得操作,返回null

resize()方法

HashMap的扩容方法。

final Node<K,V>[] resize() {
    // 用oldTab引用旧的table
    Node<K,V>[] oldTab = table;
    // 获取旧容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 获取旧扩容阈值
    int oldThr = threshold;
    // 定义新的容量和新的扩容阈值
    int newCap, newThr = 0;

    /* ---------------------- 开始设置newCap和newThr ---------------------- */

    if (oldCap > 0) {
        // 旧容量大于0的情况下,判断旧容量是否已经超过最大容量值,如果超过了,就不再扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 将扩容阈值设置为Integer.MAX_VALUE,然后直接返回旧容量
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 下面的else if 分支做了两件事:
        // 1. 将旧容量的二倍赋给新容量
        // 2. 判断新容量是否大于了最大值并且判断旧容量是否大于16
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // 将新阈值设置为旧阈值的二倍
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        // 这种情况是使用了非默认构造方法创建的map,第一次插入元素会走到这里
        newCap = oldThr;// 将新容量直接设置为旧的阈值大小
    else {               // zero initial threshold signifies using defaults
        // 调用默认构造方法创建的map,第一次插入元素会走到这里
        // 如果容量和扩容阈值都是0,也就是没有初始化
        // 那么就将新容量设置为默认值16
        // 新阈值设置为 (int)(0.75f * 16)
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        // 如果新的扩容阈值是0, 就需要对其进行赋值操作
        // 根据新容量计算出库容阈值
        float ft = (float)newCap * loadFactor;
        // 保证新的阈值不能超过最大容量值,如果没有超过就直接复制,否则就赋值为Integer.MAX_VALUE
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }

    /* ---------------------- 设置newCap和newThr完成 ---------------------- */

    // 更新库容阈值
    threshold = newThr;
    // 创建一个新容量大小的Node数组
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab; // 将哈希表更新为新创建的Node数组
    // 如果原来的就数组不为null,就进行元素的搬迁操作
    if (oldTab != null) {
        // 遍历旧数组
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e; //
            // 一开始先将哈希表中的第一个元素赋值给e,再判断是否为null
            if ((e = oldTab[j]) != null) {
                // 设置为null,以便GC垃圾回收
                oldTab[j] = null;

                // 可以将table数组中的每个元素都看成一个链表的头节点或是红黑树的根节点
                // 如果e.next == null 说明这个位置上只有一个元素没有形成链表或者红黑树
                // 这样就直接计算出该元素在新的哈希表中的位置,直接放到新的哈希表中
                // 因为每次都扩容两倍,所以这里的第一个元素搬移到新的哈希表的时候新哈希表肯定还没有元素
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    // 如果这个元素是一个树节点那么就把这棵树分成两棵树存到新的哈希表中
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    // 此时的情况是在哈希表中的一个上不是一个单个元素也不是红黑树, 而是一个链表
                    // 这时需要将链表分成两个链表插入到新的哈希表中

                    // 需要定义两对指针, 一对用于引用低位链表的头和尾,一对用于引用高位链表的头和尾
                    //
                    // 高位链表: 扩容后所在的下标位置与未扩容时一致
                    Node<K,V> loHead = null, loTail = null;
                    // 低位链表: 扩容后的下标位置 = 原来位置的下标 + 原来数组的长度
                    Node<K,V> hiHead = null, hiTail = null;

                    Node<K,V> next; // 用于表示当前链表的下一个元素
                    do {
                        // next先指向e的下一个元素,就是e和next两个指针一前一后遍历整个链表
                        next = e.next;
                        // (e.hash & oldCap) == 0 的情况应该放到低位链表中
                        // 为什么要 & oldCap的值呢?
                        // 因为容量值都是2的幂数,所以它们的二进制一般都是1 0000 或者10 0000 等
                        // 而元素的哈希值可能是 0 1111 或者 1 1111之类的值,如果用1 0000&上前者,结果就是0,而后者不是0
                        // 从而可以区分高低位元素
                        // 所以判断 & oldCap的可以分出高低位元素
                        if ((e.hash & oldCap) == 0) {
                            // 低位元素,插入到低位链表操作
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        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;
}

总结:

  1. 一开始使用的是默认构造方法时,在插入第一个元素时进行库容操作,初始化为默认值,容量为16,扩容阈值为12
  2. 如果使用 的是非默认构造方法,则第一次插入元素时初始化容量等于扩容门槛,扩容门槛在构造方法里等于传入容量向上最近的2的n次方。
  3. 如果旧容量大于0,则新容量等于旧容量的2倍,但不超过最大容量2的30次方,新扩容门槛为旧扩容门槛的2倍。
  4. 扩容操作
    • 计算出新的容量和新的阈值。
    • 根据新容量创建出新的哈希表。
    • 将旧哈希表中的元素搬迁到新的哈希表中(根据元素种类分类处理)。
      • 单个元素:计算出新的位置e.hash & (newCap - 1),存放到新的哈希表中。
      • 红黑树类型:调用split方法将树分成两棵树存放到哈希表中。
      • 链表类型:判断e.hash & oldCap的结果值将链表划分成高位链表和低位链表,低位链表还是放到原来的位置上,高位链表的存放位置变为原来的位置加上哈希表原长。
  5. 返回新的哈希表

get(Object key)方法

根据传入的key,获取value

public V get(Object key) {
    Node<K,V> e;
    // 实际实现功能的是getNode方法,根据key的哈希值和key,获取对应的Node元素,返回其value值
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    // tab: 用于引用哈希表
    // first: 用来表示哈希表中某个位置上的第一个元素
    // n: 表的大小
    // k: key值
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

    // 判断哈希表是否为null、是否为空,并且待查找的key所在的位置是否为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 检查此位置上的第一个元素是否就是目标值,是就直接返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            // 此位置上元素不止一个时的情况
            if (first instanceof TreeNode)
                // 如果是红黑树结构就调用getTreeNode方法从红黑树中获取值
                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;
}

总结:

  1. get方法实际是调用了getNode方法实现的功能
  2. 计算key的哈希值
  3. 根据哈希值找到所在哈希表中的位置
  4. 如果对应位置上只有一个元素就判断这个元素是不是我们要找的值
  5. 如果不是判断是否为链表或者红黑树结构,如果是就在链表或者红黑树上查找元素
  6. 如果未找到,返回null

remove(Object key)

根据指定的key删除元素。

public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    // tab: 引用当前表
    // p: 引用表中的元素
    // n: 存储表长
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 判断表是否已经创建、是否有元素并且根据hash值算出的位置上是否有元素
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v;
        // 此if中的情况是判断根据hash值找到的位置上的第一个元素是否就是要删除的元素
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            // 找出的位置上不止一个元素并且第一个不是要删除的元素的情况
            // 判断是否为红黑树结构
            if (p instanceof TreeNode)
                // 调用getTreeNode方法获取结点
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else {
                // 从链表中找到要删除的元素
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }

        // 如果找到了元素,则看参数是否需要匹配value值,如果不需要匹配直接删除,如果需要匹配则看value值是否与传入的value相等
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            // 如果要删除的元素属于红黑树结构,如果是就调用树的删除方法
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                // 如果待删除的元素是第一个元素,则把第二个元素移到第一的位置
                tab[index] = node.next;
            else
                // 删除链表中节点操作
                p.next = node.next;
            ++modCount;
            --size;
            // 删除节点后置处理
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

总结:

  1. 先计算hash值
  2. 根据hash值找到对应哈希表中的位置
  3. 判断指定位置上是否正好就是要删除的元素,是就后续进行删除操作,如果不是,就判断是不是链表或者红黑树,如果是就对其进行响应的删除操作
  4. 修改size值,modCount值以及后续处理

总结

  1. HashMap底层是哈希表,使用(数组 + 链表 + 红黑树)
  2. ashMap查找添加元素的时间复杂度都为 O ( 1 ) O(1) O(1)
  3. 默认容量是16(1 << 4),如果一开始没有指定容量,就在第一次调用put方法时初始化容量
  4. 每次扩容为原来容量的二倍
  5. 存在一个扩容阈值,这个值是根据负载因子(0.75f)和容量大小动态计算的
  6. 当哈希表大小不超过64时不会进行树化操作
  7. 当一个位置上链表长度大于8并且哈希表大小大于64时会对链表进行树化操作
  8. 当哈希表中的某一位置上元素个数小于6时,进行反树化操作
  9. HashMap不是线程安全的,而HashTable是线程安全的

面试题: 为什么HashMap为什么每次扩容的倍数是2 ?

正常来讲取模使用 % ,但是Java对其进行了优化,就是使用 位运算(&)进行优化,而想要这样优化就需要把HashMap内部的数组长度固定为2的幂的长度,所以扩容都是原来的二倍。

HashMap中的tableSizeFor方法根据传入的容量大小后经过位运算处理过的元素都是2的幂,这样处理好处是可以使添加的元素更均匀的分布在HashMap底层的数组中,减少hash碰撞,避免形成链表的结构,使得查询效率降低。

tableSizeFor方法的实现:

/**
 * 返回给定目标容量的最近的 2 次方数。
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

参考文章

彤哥读源码: 死磕 java集合之HashMap源码分析

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-04-18 18:07:50  更:2022-04-18 18:11:00 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 7:47:33-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码