HashMap

1. 基础

1.1 理论

JDK1.8 之前 HashMap数组+链表组成的,每个数组元素存储一个链表的头结点。数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。

JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。 可以存储 nullkeyvalue

HashMap

1.2 计算哈希值

使用 key 计算。

哈希表内部数组的大小很重要,要保持一个平衡的数字,不能让哈希碰撞太频繁,也不能占用空间太大。

在哈希表使用的过程中,会不断的调整数组的容量。

  • 调整后的容量是多少?之前的2倍

  • 如何调整?再散列调整数组大小。数组长度小于64,会先扩充数组。

2. 扩容源码

每个节点的定义如下:

在这里插入图片描述

HashMap底层是一个数组,元素的类型是Node节点,源码如下:

在这里插入图片描述

在添加元素put(key, value)的时候,会调用putValue()进行实际的放入过程。

3

putValue()在加入一个节点之后,会判断是否超过容量,如果超了,会调用resize()进行2倍扩容。源码如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果当前表为null,或者表为空,则初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;    // 通过resize()初始化表,默认表长为10
    // 获取该hash值在表中的位置 i
    // 1. 如果该table[i]为null,说明该hash值没有存放其他值,放入
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 创建一个新节点存放在表中
        tab[i] = newNode(hash, key, value, null);
    // 2. 如果table[i]不为null,说明当前值与表中已有的值冲突了,那么加在该位置的链表最后
    else {
        Node<K,V> e;   // 表中的某个节点,和当前对象的key相等
        K k;
        // 当前的键 key 已存在
        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;
                }
                // 如果表中有元素和当前值相等,则跳出
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 表中存在某个值和当前值相等,更新为当前值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    // 当前容量 +1 后超过阈值,则扩容
    if (++size > threshold)
        resize();    // 2倍扩容
    afterNodeInsertion(evict);
    return null;
}

resize()扩容

在这里插入图片描述

如果进行2倍扩容,也需要将表中已有的值,重新散列到新表中,重新散列的过程是通过位运算计算新键值,而不是重新计算一遍散列函数。resize()中相关代码如下:

在这里插入图片描述

链表转红黑树的方法treeifyBin()

在这里插入图片描述

小纸条:(n-1) & hash

n 是2的幂次数,其二进制表示是 10...0,那么 n-1的二进制表示是01...1

(n-1) & hash 是一个与运算,位与位进行与运算。其结果就是将任意一个值对n的余数,相当于 hash % n。位运算的效率更高。

3. 缺点

hashmap 高度依赖于 hash 算法,如果 key 是自定义类,需要自己重写 hashcode() 方法,写 hash 算法。

4. 并发产生的问题

在调整大小的过程中,有一步是把老数组中的全部元素转移到新数组中。这个过程在并发环境中会发生错误,导致数组链表中的链表形成循环链表。1.8之前是头插法,会导致循环链表的产生。1.8以后是尾插法,不会导致循环链表的产生。

// 在1.8中没有看到这个函数
void transfer( Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++){
        Entry<K,V> e = src[j];
        if (e != null){
            src[j] = null;
            do {
                //假设第一个线程执行到这里因为某种原因挂起
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash,newCapacity);e.next = newTable[i];
                newTable[i] = e;
                e = next;
                }while (e != null);
        }
    }
}

java 8 会出现什么问题?

java 8不会出现再散列时形成的循环链表,会造成数据覆盖。

在插入数据时会判断是否出现哈希碰撞,判断完之后正常插入。这里会出现一个线程的数据覆盖另一个线程的数据。

HashMap 在于并发下的 Rehash 再散列会造成元素的覆盖问题,所以不能在多线程下使用。

在这里插入图片描述
(图片来源于网络)

### Java 中 HashMap 的使用方法 #### 1. **简介** `HashMap` 是 Java 集合框架中的一个重要类,用于存储键值对(key-value pair)。它的底层基于哈希表实现,提供了快速的查找、插入和删除操作。`HashMap` 不保证元素的顺序,并允许一个 `null` 键和多个 `null` 值。 #### 2. **基本操作** 以下是 `HashMap` 的一些常用方法及其功能: - **put(key, value)**: 将指定的键值对存入 `HashMap`。 - **get(key)**: 返回与指定键关联的值。 - **remove(key)**: 移除指定键对应的映射关系。 - **size()**: 返回 `HashMap` 中键值对的数量。 - **clone()**: 创建并返回该 `HashMap` 对象的一个副本。 - **isEmpty()**: 如果此 `HashMap` 映射不包含任何键值对,则返回 true。 这些方法的具体用法可以通过下面的例子来说明。 --- #### 3. **示例代码** ##### 示例 1: 添加键值对并获取大小 ```java // Java program to demonstrate the use of size() method in HashMap import java.util.*; public class SizeExample { public static void main(String[] args) { // Create an empty HashMap Map<Integer, String> map = new HashMap<>(); // Add key-value pairs using put() map.put(10, "C"); map.put(20, "C++"); map.put(50, "JAVA"); map.put(40, "PHP"); map.put(30, "SFDC"); // Print the HashMap content System.out.println("HashMap Content: " + map); // Get the number of entries in the HashMap int size = map.size(); System.out.println("Size of HashMap: " + size); } } ``` 这段代码展示了如何向 `HashMap` 插入数据以及计算其大小[^1]。 --- ##### 示例 2: 删除特定键值对 ```java // Java program to demonstrate the removal operation in HashMap import java.util.*; public class RemoveExample { public static void main(String[] args) { // Initialize a HashMap with some data Map<Integer, String> map = new HashMap<>(); map.put(10, "C"); map.put(20, "C++"); map.put(50, "JAVA"); map.put(40, "PHP"); map.put(30, "SFDC"); // Display initial state System.out.println("Initial HashMap: " + map); // Remove entry associated with key '50' map.remove(50); // Show updated HashMap after deletion System.out.println("Updated HashMap after removing key '50': " + map); } } ``` 这里演示了如何移除某个键所对应的数据项[^2]。 --- ##### 示例 3: 复制一份新的 HashMap 实例 ```java // Example demonstrating cloning functionality within HashMaps. import java.util.*; public class CloneExample { public static void main(String[] args) { // Original HashMap creation and population HashMap<Integer, String> originalMap = new HashMap<>(); originalMap.put(10, "C"); originalMap.put(20, "C++"); originalMap.put(50, "JAVA"); originalMap.put(40, "PHP"); originalMap.put(30, "SFDC"); // Cloning process begins here HashMap<Integer, String> clonedMap = (HashMap<Integer, String>)originalMap.clone(); // Output both maps post-cloning action System.out.println("Original HashMap Contents: " + originalMap); System.out.println("Cloned HashMap Contents: " + clonedMap); } } ``` 本部分解释了复制现有 `HashMap` 的过程[^3]。 --- #### 4. **性能特点与其他注意事项** 由于 `HashSet` 内部依赖于 `HashMap` 来管理其成员集合,因此它们共享相似的时间复杂度特性——平均情况下 O(1),最坏情况取决于冲突处理机制[^4]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值