聊聊 HashMap

引言

JDK 版本 1.8

我为什么要学习 HashMap

HashMap 是 Java 中非常重要的数据结构,在 Java 中几乎无处不在,几乎所有的框架都会用到 HashMap,比如 Spring、MyBatis、Dubbo 等等。HashMap 的重要性不言而喻,所以我有必要对 HashMap 进行深入的学习。

HashMap 概述

特点

  • 是一种键值存储的数据结构,它的存储方式是将键和值进行一一映射,通过键来计算出值的存储地址,然后将值存储到该地址中。
  • 允许使用任何类型的键和值,包括自定义的类型。
  • 在使用过程中,通过键来计算出值的存储地址,然后从该地址中取出值。这种存储方式使得 HashMap 能够快速的存储和检索数据。
  • 是一种非线程安全的数据结构,如果多个线程同时对 HashMap 进行操作,可能会导致数据的不一致。
  • 允许使用 null 作为键和值,但是建议尽量避免使用 null 作为键,因为如果使用 null 作为键。
  • 存储元素的顺序是不确定的,也就是不能通过迭代器对 HashMap 进行遍历来保证元素的顺序。
  • 随着数据量的增加,会进行自动扩容,以提供更好的性能。

源码学习

继承关系

HashMap 的关系图

  • Map<K,V>:是一种键值对的数据结构接口,定义了 Map 对象的基本操作
  • AbstractMap<K,V>:主要是提供了一个 Map 接口的默认实现,以及为 Map 接口的一些方法提供了默认的实现
  • Serializable:标识当前类的对象可以被序列化
  • Cloneable:用于标记当前类可以进行克隆操作,但是让需要调用 Object.clone() 方法进行浅拷贝

创建对象

在平常使用的过程中,我们通常会使用如下方式来创建 HashMap 对象:

import java.util.HashMap;
import java.util.Map;

Map<String, String> map = new HashMap<>();

其中 Map 需要导入 java.util.Map 包,HashMap 需要导入 java.util.HashMap 包。

创建细节

创建一个 Map 的方式通常是使用 new 关键字来创建,回去调用 HashMap 的构造方法,那就从构造方法去看吧:


// 在构造函数中未指定任何内容时使用的荷载系数(哈希表中元素的数量与哈希表的大小之间的比例)扩容的关键因素之一
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 哈希表的负载因子(已存储的键值对数量与哈希表容量之间的比率)扩容的关键因素之一
final float loadFactor;

/**
 * 使用默认初始容量 (16) 和默认负载系数 (0.75) 构造一个空的 HashMap
 * 也就是:new HashMap<>(16, 0.75f);
 */
public HashMap() {     
    this.loadFactor = DEFAULT_LOAD_FACTOR; // 所有其他字段默认
}




// 接下来就看看默认的构造方法吧,也是个有参构造




// 最大容量,如果任一构造函数使用参数隐式指定了较高的值,则使用该容量。必须是 2 的幂<= 1<<30  (位移运算:1073741824)
static final int MAXIMUM_CAPACITY = 1 << 30;

// 要调整大小的下一个大小值(容量 * 负载系数)
int threshold;

/**
 * 构造一个具有指定初始容量和负载因子的空 HashMap
 *
 * @param  initialCapacity 初始容量
 * @param  loadFactor      负载系数
 * @throws IllegalArgumentException 如果初始容量为负或负载系数为非正
 */
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))
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    // 设置加载因子
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

// 对于给定的目标容量,返回大小为 2 的幂
static final int tableSizeFor(int cap) {
    // 位运算,将 cap - 1 的二进制位全部置为 1
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    // 如果 cap - 1 是 2 的幂,则返回 cap,否则返回 cap - 1 的二进制位全部置为 1 的值 + 1
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

简单使用

增加

  • put

这个方法存在与 Map 接口中的 V put(K key, V value);

简单使用

Map<String, String> map = new HashMap<>();
map.put("name", "张三"); 
String age = map.put("age", "18");

将指定值与此映射中的指定键进行关联。如果映射之前包含键的映射,则替换旧值。

源码分析

// put 方法
public V put(K key, V value) {
    // 调用 putVal 方法,将 key 和 value 传入
    return putVal(hash(key), key, value, false, true);
}

// 在首次使用时初始化,并根据需要调整大小。分配时,长度始终是 2 的幂。(用来存储数据的)
transient Node<K,V>[] table;

// 此 HashMap 在结构上被修改的次数
transient int modCount;

// 此映射中包含的键值映射数
transient int size;

// 要调整大小的下一个大小值(容量 * 负载系数)
int threshold;

// 使用树而不是列表的箱计数阈值
static final int TREEIFY_THRESHOLD = 8;

// hash 方法,计算 key 的哈希值
static final int hash(Object key) {
    int h;
    // key 为 null,则返回 0,否则返回 key 的哈希值
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

/**
 * 实现 Map.put 和相关方法。
 *
 * @param hash 键的哈希值
 * @param key 键
 * @param value 要放置的值
 * @param onlyIfAbsent 如果为 true,则不要更改现有值
 * @param evict 如果为 false,则表处于创建模式。
 * @return 上一个值,如果没有,则为 null
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K, V>[] tab; Node<K, V> p; int n, i;
    // table 为当前 Map 中的数组,如果为空,则调用 resize() 方法进行初始化
    if ((tab = table) == null || (n = tab.length) == 0) 
        // 初始化数组
        n = (tab = resize()).length;
    // 计算出数组中的索引
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 如果索引位置为空,则直接将 key 和 value 封装成 Node 对象,然后放入数组中
        tab[i] = newNode(hash, key, value, null);
    else {
        // 如果索引位置不为空,则进行判断
        Node<K, V> e; K k;
        // 如果 key 和 hash 值都相同,则直接将 value 覆盖
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            // 将 p 赋值给 e
            e = p;
        // 如果 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 和 value 封装成 Node 对象,然后放入数组中
                if ((e = p.next) == null) {
                    // 将 key 和 value 封装成 Node 对象,然后放入数组中
                    p.next = newNode(hash, key, value, null);
                    // 如果链表长度大于 8,则将链表转换为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 将链表转换为红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                // 如果 key 和 hash 值都相同,则直接将 value 覆盖
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) 
                    break;
                p = e;
            }
        }
        // 如果 e 不为空,则说明 key 和 hash 值都相同(通过上面的操作找到了重复的键),则直接将 value 覆盖
        if (e != null) { // existing mapping for key
            // 保存旧值(用作返回)
            V oldValue = e.value;
            // 如果 onlyIfAbsent 为 false,则直接将 value 覆盖(在 put 方法中传的参数,首次使用)
            if (!onlyIfAbsent || oldValue == null) 
                // 将 value 覆盖
                e.value = value;
            // 在 HashMap 中为空的方法体,主要作用与 LinkedHashMap 中使用
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 此 HashMap 在结构上被修改的次数
    ++modCount;
    // 如果 size 大于阈值,则进行扩容
    if (++size > threshold) 
        resize();
    // 在 HashMap 中为空的方法体,主要作用与 LinkedHashMap 中使用
    afterNodeInsertion(evict);
    return null;
}

下面用一张网图再看看吧:(网图)

HashMap.put 的流程图

  • putAll

这个方法存在与 Map 接口中的 void putAll(Map<? extends K, ? extends V> m);

简单使用

Map<String, String> map = new HashMap<>();
Map<String, String> map2 = new HashMap<>();
map2.put("name", "张三"); 
map2.put("age", "18");
map.putAll(map2);

源码分析

/**
 * 将指定映射中的所有映射复制到此映射。这些映射将替换此映射对指定映射中当前任何键的任何映射。
 *
 * @param m 要存储在此映射中的映射
 * @throws NullPointerException 如果指定的映射为 null
 */
public void putAll(Map<? extends K, ? extends V> m) {     
    putMapEntries(m, true);
}

/**
 * 实现 Map.putAll 和 Map 构造函数。
 *
 * @param m 要存储在此映射中的映射
 * @param evict 如果为 false,则表处于创建模式。如果为 true,则表已初始化其阈值,因此,如果添加更多元素,则不会调整大小。
 */
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    // 获取 m 的大小
    int s = m.size();
    // 如果 m 不为空
    if (s > 0) {
        // 如果 table 为空,则调用 inflateTable 方法进行初始化
        if (table == null) { // pre-size
            // 获取 m 的负载因子
            float ft = ((float)s / loadFactor) + 1.0F;
            // 获取 m 的大小,如果 m 的大小大于最大容量,则将阈值设置为最大容量,否则将阈值设置为大于 m 的大小的最小的 2 的幂
            int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
            // 如果阈值大于阈值,则将阈值设置为大于 m 的大小的最小的 2 的幂
            if (t > threshold)
                // 设置阈值
                threshold = tableSizeFor(t);
        }
        // 如果 m 的大小大于阈值,则进行扩容
        else if (s > threshold)
            resize();
        // 遍历 m
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            // 调用 putVal 方法,将 key 和 value 传入,也就是调用 put 方法
            putVal(hash(key), key, value, false, evict);
        }
    }
}
  • putIfAbsent

同样是存在于 Map 接口中的 V putIfAbsent(K key, V value);

相当于(从源码中扒拉出来的):

// 如果不存在 key,则将 key 和 value 封装成 Node 对象,然后放入数组中
V v = map.get(key);
if (v == null)
    v = map.put(key, value);
return v;

简单使用

Map<String, String> map = new HashMap<>();
map.put("name", "张三");
String age = map.putIfAbsent("age", "18");

源码分析

注意一下和 put 方法的区别:都是调用 putVal 方法,只是在 putVal 方法中的判断条件不同。

第四个参数为 true,表示如果存在 key,则不要更改现有值,如果不存在 key,则进行添加。

// putIfAbsent 方法
public V putIfAbsent(K key, V value) {     
    return putVal(hash(key), key, value, true, true); 
}

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

删除

  • remove(Object key)

这个方法存在与 Map 接口中的 V remove(Object key);

简单使用

Map<String, String> map = new HashMap<>();
map.put("name", "张三");
String name = map.remove("name");

源码分析

/**
 * 从此映射中删除指定键的映射(如果存在)。
 *
 * @param  key 键,其映射将从映射中删除
 * @return 与 key 关联的上一个值,如果没有 key 的映射,则为 null。(null 返回还可以指示映射以前将 null 与 key 关联。
 */
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}

/**
 * 实现 Map.remove 和相关方法。
 *
 * @param 键的哈希哈希
 * @param key 键
 * @param value 如果 matchValue,则为要匹配的值,否则忽略
 * @param matchValue 如果为 true,则仅在值相等时删除
 * @param movable 如果为 false,则在删除时不移动其他节点
 * @return 节点,如果没有,则为 null
 */
final Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    // 判断 table 是否为空,或者 table 的长度为 0,如果是,则直接返回 null
    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;
        // 如果 p 的 hash 值和 key 都相同,则直接将 p 赋值给 node
        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)
                // 如果是红黑树,则调用红黑树的方法进行查找
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else { // 如果是链表,则遍历链表
                do { // 遍历链表
                    // 如果 e 的 hash 值和 key 都相同,则直接将 e 赋值给 node
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                    // 将 e 的下一个节点赋值给 e
                } while ((e = e.next) != null);
            }
        }
        // 如果 node 不为空,则说明找到了要删除的节点,进行删除操作
        if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) {
            // 如果 node 是 TreeNode 类型,则调用红黑树的方法进行删除
            if (node instanceof TreeNode)
                // 调用红黑树的方法进行删除
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p) // 如果 node 是链表的第一个节点,则将 node 的下一个节点赋值给 tab[index]
                tab[index] = node.next;
            else // 如果 node 不是链表的第一个节点,则将 node 的下一个节点赋值给 node 的上一个节点的下一个节点
                p.next = node.next;
            // 此 HashMap 在结构上被修改的次数
            ++modCount;
            // 记录table存储了多少键值对,因为移除了一个,所以此处就减一
            --size;
            // 在 HashMap 中是空方法,主要是供 LinkedHashMap 使用,因为 LinkedHashMap 重写了该方法
            afterNodeRemoval(node);
            // 返回 node (被删除的节点)
            return node;
        }
    }
    return null;
}

整个流程和 put 方法差不多,也是先判断 table 是否为空,然后判断是否为红黑树,然后判断是否为链表,然后进行删除操作。主要是在删除操作中,如果是红黑树,则调用红黑树的方法进行删除,如果是链表,则进行遍历,找到要删除的节点,然后进行删除操作。

  • remove(Object key, Object value)

这个方法存在与 Map 接口中的 boolean remove(Object key, Object value);

简单使用

Map<String, String> map = new HashMap<>();
map.put("name", "张三");
boolean result = map.remove("name", "张三");

相当于:

if (map.containsKey(key) && Objects.equals(map.get(key), value)) {
    map.remove(key);
    return true;
} else
    return false;

源码分析

注意一下和 remove(Object key) 方法的区别:都是调用 removeNode 方法,只是在 removeNode 方法中的判断条件不同。
第四个参数为 true,表示如果存在 key,则不要更改现有值,如果不存在 key,则进行添加。

// remove(Object key, Object value) 方法
public boolean remove(Object key, Object value) {
    return removeNode(hash(key), key, value, true, true) != null;
}

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

查询

  • get

存在于 Map 接口中的 V get(Object key);

简单使用

Map<String, String> map = new HashMap<>();
map.put("name", "张三");
String name = map.get("name");

源码分析

// 通过 key 获取 value
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

/**
 * 实现 Map.get 和相关方法。
 *
 * @param hash 键的哈希值
 * @param key 键
 * @return 节点,如果没有,则为 null
 */
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 判断 table 是否为空,或者 table 的长度为 0,如果是,则直接返回 null
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        // 始终检查第一个节点
        // 如果第一个节点的 hash 值和 key 都相同,则直接将 first 赋值给 e
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 如果第一个节点不相同,则判断是否为红黑树
        if ((e = first.next) != null) {
            // 如果是红黑树,则调用红黑树的方法进行查找
            if (first instanceof TreeNode)
                // 调用红黑树的方法进行查找
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do { // 遍历链表
                // 如果 e 的 hash 值和 key 都相同,则直接将 e 赋值给 e
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null); // 将 e 的下一个节点赋值给 e
        }
    }
    return null;
}
  • getOrDefault

存在于 Map 接口中的 `default V getOrDefault(Object key, V defaultValue)

简单使用

Map<String, String> map = new HashMap<>();
map.put("name", "张三");
String name = map.getOrDefault("name", "李四");

源码分析

注意和 get 方法的区别:都是调用 getNode 方法,只是在获取到节点后的操作不同。

// getOrDefault 方法
public V getOrDefault(Object key, V defaultValue) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}

// get 方法
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

常见问题

为什么只能存储一个 null 键

存储最终都会通过计算出键的哈希值,先看看对应的方法

// hash 方法,计算 key 的哈希值
static final int hash(Object key) {
    int h;
    // key 为 null,则返回 0,否则返回 key 的哈希值
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

可以看出,当键为 null 的时候,返回的哈希值为 0,而哈希值为 0 的时候,存储的位置为数组的第一个位置,所以 HashMap 只能存储一个 null 键。

如何遍历

  • 第一种方式:通过 keySet() 方法获取所有的键,然后通过键获取值

对于数据量较小的效率较高,但是对于数据量较大的效率较低,由于需要复制内存,所以效率较低。

Set<String> keySet = map.keySet();
for (String key : keySet) {
    System.out.println(key + ":" + map.get(key));
}
  • 第二种方式:通过 entrySet() 方法获取所有的键值对,然后通过键值对获取键和值
Set<Map.Entry<String, String>> entrySet = map.entrySet();
for (Map.Entry<String, String> entry : entrySet) {
    System.out.println(entry.getKey() + ":" + entry.getValue());
}
  • 第三种方式:通过 values() 方法获取所有的值
Collection<String> values = map.values();
for (String value : values) {
    System.out.println(value);
}
  • 第四种方式:通过 forEach() 方法获取所有的键值对,然后通过键值对获取键和值
map.forEach((key, value) -> {
    System.out.println(key + ":" + value);
});
  • 第五种方式:通过 Iterator 迭代器获取所有的键值对,然后通过键值对获取键和值(和第二种类似)

虽然避免了内存复制,但是在遍历的过程中,如果对 HashMap 进行了修改,则会抛出 ConcurrentModificationException 异常。也不会保证遍历顺序。

Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<String, String> entry = iterator.next();
    System.out.println(entry.getKey() + ":" + entry.getValue());
}
  • 第六种方式:通过 Stream 流获取所有的键值对,然后通过键值对获取键和值

在处理数据量较小的情况下效率较低,因为并行处理的开销比较大,但是在处理数据量较大的情况下效率较高。

map.entrySet().forEach(entry -> {
    System.out.println(entry.getKey() + ":" + entry.getValue());
});
  • 第七种方式:通过 Stream 流获取所有的键值对,然后通过键值对获取键和值(和上面使用的流不同)
map.entrySet().parallelStream().forEach(entry -> {
    System.out.println(entry.getKey() + ":" + entry.getValue());
});

那种遍历效率较好

要根据其内部结构来进行分析,HashMap 的内部结构是数组 + 链表 + 红黑树,所以遍历效率较好的应该是:

  • 使用迭代器(上述第五种)
  • 使用 for-each(上述第二种)
  • 使用 forEach 和 lambda 表达式(上述第四种)
  • 使用 stream 流(上述第六种)

具体选择那种方法,可以根据实际情况进行选择,如果是在单线程环境下,建议使用 for-each,如果是在多线程环境下,建议使用迭代器。

怎么扩容

在上面的 put 方法中,有这么一段代码:

// 如果 size 大于阈值,则进行扩容
if (++size > threshold) 
    resize();

也就是说 resize() 方法是进行动态扩容的关键:

// 最大容量,如果任一构造函数使用参数隐式指定了较高的值,则使用该容量。必须是 2 的幂<= 1<<30  (位移运算:1073741824)
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认初始容量(必须是 2 的幂)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

// 在构造函数中未指定任何内容时使用的荷载系数
static final float DEFAULT_LOAD_FACTOR = 0.75f;

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

final Node<K,V>[] resize() {
    // 将 table 赋值给 oldTab,对扩容前的数组进行保存
    Node<K,V>[] oldTab = table;
    // 获取 oldTab 的长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 获取 oldTab 的阈值
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 如果 oldCap 大于 0。也就是说,如果 oldTab 不为空,则进行扩容
    if (oldCap > 0) {
        // 如果 oldCap 大于最大容量,则将阈值设置为最大容量,并终止扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 如果 oldCap * 2 小于最大容量,并且 oldCap 大于等于默认容量,则将 newCap 设置为 oldCap * 2,newThr 设置为 oldThr * 2
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // 双阈值
    }
    // 如果 oldThr 大于 0(也就是 threshold 为 0 ),则将 newCap 设置为 oldThr
    else if (oldThr > 0) // 初始容量处于阈值
        newCap = oldThr;
    else {               // 零初始阈值表示使用默认值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        // ft 为 newCap * 0.75,如果 ft 小于最大容量,并且 newCap 小于最大容量,则将 newThr 设置为 ft,否则将 newThr 设置为最大容量
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    // 使用 loadeFactor 重新设置阈值,保存在 threshold 中
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    // 创建一个新的数组,长度为 newCap
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    // 将新创建的数组赋值给 table,完成 table 的初始化(扩容)
    table = newTab;
    // 如果 oldTab 不为空(扩容前的数组中存在数据),则进行数据迁移,迁移到扩容后的数组中
    if (oldTab != null) {
        // 对旧数组进行遍历
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 如果旧数组中当前节点的元素不为空,则进行数据迁移
            if ((e = oldTab[j]) != null) {
                // 将旧数组中当前节点的元素置为 null,方便垃圾回收
                oldTab[j] = null;
                // 如果当前节点的下一个节点为空,则直接将当前节点放入新数组中
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                // 如果当前节点的下一个节点不为空,则需要将当前节点的链表进行拆分,然后再放入新数组中
                else if (e instanceof TreeNode)
                    // 如果当前节点是红黑树,则调用 split 方法进行拆分
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // 否则就是链表
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do { // 遍历链表
                        // 将当前节点的下一个节点赋值给 next
                        next = e.next;
                        // 如果当前节点的 hash 值和 oldCap 相与为 0,则将当前节点放入 loHead 中
                        if ((e.hash & oldCap) == 0) {
                            // 如果 loTail 为空,则将 loHead 和 loTail 都设置为当前节点
                            if (loTail == null)
                                loHead = e;
                            // 如果 loTail 不为空,则将 loTail 的下一个节点设置为当前节点
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else { // 否则就是 oldCap
                            // 如果 hiTail 为空,则将 hiHead 和 hiTail 都设置为当前节点
                            if (hiTail == null)
                                hiHead = e;
                            // 如果 hiTail 不为空,则将 hiTail 的下一个节点设置为当前节点
                            else
                                hiTail.next = e;
                            // 将 hiTail 设置为当前节点
                            hiTail = e;
                        }
                    } while ((e = next) != null); // 将 next 赋值给 e
                    // 如果 loTail 不为空,则将 loTail 的下一个节点设置为 null,然后将 loHead 放入新数组中
                    if (loTail != null) {
                        // 将 loTail 的下一个节点设置为 null
                        loTail.next = null;
                        // 将 loHead 放入新数组中
                        newTab[j] = loHead;
                    }
                    // 如果 hiTail 不为空,则将 hiTail 的下一个节点设置为 null,然后将 hiHead 放入新数组中
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    // 返回新数组
    return newTab;
}

简单概括一下,就是先判断原始数组是否为空,如果为空,则进行初始化,如果不为空,则判断是否需要扩容,如果需要扩容,则进行扩容,扩容后将原始数组的数据再次放入新的数组中,如果不需要扩容,则不进行扩容。

总结

  • 高性能:HashMap 在 Java 中提供了一种快速且高效的方式来存储和检索键值对。由于其内部使用了哈希表,因此能够在很短的时间内对数据进行存储和检索等操作。这种性能优势使得其成为存储和检索大量数据的重要工具。
  • 键值存储:HashMap 是一种键值存储的数据结构,它的存储方式是将键和值进行一一映射,通过键来计算出值的存储地址,然后将值存储到该地址中。当需要获取值时,通过键来计算出值的存储地址,然后从该地址中取出值。这种存储方式使得 HashMap 能够快速的存储和检索数据。
  • 动态扩容:其内部能够动态调整容量,当容量不足时,会自动扩容,以便能够存储更多的数据。当存储的数据量增加时,会自动扩容,以便能够存储更多的数据。当存储的数据量减少时,会自动缩容,以便节省内存空间,而不需要手动进行管理。
  • 线程不安全:HashMap 是线程不安全的,如果多个线程同时对 HashMap 进行操作,可能会导致数据的不一致。因此,如果需要在多线程环境下使用 HashMap,需要进行额外的同步处理。因此,在多线程环境下,建议使用线程安全的 Map 进行实现,如:ConcurrentHashMap。
  • 易操作:其提供了简单易用的 API,使得存储、检索和删除等操作都变得非常简单,此外还提供了许多实用的方法,如:containsKey()、containsValue()、keySet()、values()、entrySet() 等方法,使得开发人员能够快速的对数据进行操作。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值