HashMap原理

目录

介绍

原理

核心结构

核心方法与流程

插入(put方法)

扩容 (resize 方法)

查询 (get 方法)

删除 (remove 方法)

常见问题

线程安全问题

结论

原因

解决方法

null键和null值

结论

重写 equals 方法和 hashcode 方法

结论

负载因子的选择

结论

采用(哈希码 & 数组长度减一)计算方式

结论


介绍

        HashMap是java中常用的一种数据结构。HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。

        HashMap在jdk1.2中初始实现,jdk1.7及其之前采用 数组+链表 方式实现,jdk1.8优化为 数组 + 链表 + 红黑树 的实现方式。

原理

注:以下基于jdk1.8

核心结构

        HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口,其定义和核心参数如下:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    transient Node<K,V>[] table; // node数组
    transient Set<Map.Entry<K,V>> entrySet; // 存储具体元素的集合
    transient int size; // 记录map的数据个数
    transient int modCount; // HashMap扩容和结构改变的次数
    int threshold; // 记录要调整大小的临界值
    final float loadFactor; // 负载因子

    // 默认初始容量 - 必须是2的幂
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 链表转红黑树的阈值
    static final int TREEIFY_THRESHOLD = 8;
    // 红黑树转链表的阈值
    static final int UNTREEIFY_THRESHOLD = 6;
    // 树化的最小容量
    static final int MIN_TREEIFY_CAPACITY = 64;

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
    }

    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
    }
}
  • 数据结构图:

        数据存储采用数组+链表+红黑树的方式,当链表长度超过阈值(默认 8)且数组容量 ≥ 64 时,链表会转换为红黑树;若节点数减少到 6 以下,红黑树会退化为链表(避免频繁树结构调整)。

核心方法与流程

插入(put方法)

  • 底层代码

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

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

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        HashMap.Node<K,V> e; K k;
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof HashMap.TreeNode)
            e = ((HashMap.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);
                    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;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

final void treeifyBin(HashMap.Node<K,V>[] tab, int hash) {
    int n, index; HashMap.Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        HashMap.TreeNode<K,V> hd = null, tl = null;
        do {
            HashMap.TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}
  • 流程解析

扩容 (resize 方法)

  • 底层代码
final HashMap.Node<K,V>[] resize() {
    HashMap.Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        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
        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"})
    HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            HashMap.Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof HashMap.TreeNode)
                    ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    HashMap.Node<K,V> loHead = null, loTail = null;
                    HashMap.Node<K,V> hiHead = null, hiTail = null;
                    HashMap.Node<K,V> next;
                    do {
                        next = e.next;
                        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;
}
  • 流程解析

  • 注:设置高低位链表,采用 j 和 j + oldcap 定位新位置可以避免重新哈希,提高性能,其值与重新哈希计算位置是相同的,以下是一个简单举例:

假设:

旧容量 = 16(掩码 1111

Key的哈希值:... 0001 1010(二进制)

旧位置计算

hash    : ???? ???? ???1 1010  
oldCap-1: 0000 0000 0000 1111
index   : 0000 0000 0000 1010 → 十进制10 (j=10)

新位置计算

newCap-1: 0000 0000 0001 1111
hash    : ???? ???? ???1 1010
index   : 0000 0000 0001 1010 → 十进制26

通过 j + oldCap 验证

10 (j) + 16 (oldCap) = 26

查询 (get 方法)

  • 底层代码
public V get(Object key) {
    HashMap.Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final HashMap.Node<K,V> getNode(int hash, Object key) {
    HashMap.Node<K,V>[] tab; HashMap.Node<K,V> first, e; int n; K k;
    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 HashMap.TreeNode)
                return ((HashMap.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;
}

删除 (remove 方法)

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

final HashMap.Node<K,V> removeNode(int hash, Object key, Object value,
                                   boolean matchValue, boolean movable) {
    HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
        HashMap.Node<K,V> node = null, e; K k; V v;
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        else if ((e = p.next) != null) {
            if (p instanceof HashMap.TreeNode)
                node = ((HashMap.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);
            }
        }
        if (node != null && (!matchValue || (v = node.value) == value ||
                (value != null && value.equals(v)))) {
            if (node instanceof HashMap.TreeNode)
                ((HashMap.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;
}

常见问题

线程安全问题

结论

        HashMap是非线程安全的。

原因

  • 环形链表:在JDK 1.7版本中,HashMap在扩容时使用头插法迁移链表,这可能导致链表成环,进而在遍历链表时引发死循环,最终导致StackOverflowError;而JDK 1.8版本采用尾插法可以避免该问题。

  • 数据覆盖‌:在多线程环境下,如果多个线程同时调用 put()方法插入数据,当两个线程同时计算哈希值并定位到同一个桶(bucket)时,如果该位置为空,新插入的数据可能会覆盖之前的数据。

  • size计算不准:多线程环境下同时调用 put()remove()方法时,由于size变量的非原子性操作,可能会导致最终的大小值不准确‌。

  • 数据丢失:在扩容过程中,如果其他线程尝试插入新元素而resize操作尚未完成,可能会导致新插入的数据被遗漏‌。

解决方法

  • 使用Hashtable(已废弃):Hashtable是过时的线程安全集合,在所有方法上都使用synchronized关键字。然而,由于每个操作都被同步,效率较低,不适合高并发场景。

  • 使用ConcurrentHashMap:通过分段锁机制允许多个线程并发访问不同的段,减少锁的竞争‌。

  • 使用Collections.synchronizedMap:可以通过Collections.synchronizedMap(new HashMap<>())方法将HashMap包装成一个线程安全的Map。这种方式在每次访问Map的操作上都加了同步锁,但会降低性能。

  • 手动加锁:使用 ReadWriteLock、synchronized等工具加锁保证线程安全。但需要开发者手动管理锁,会稍显复杂。

null键和null值

结论

        允许 null 键值,null 键在 HashMap 中仅有一个(唯一性),存储于数组起始位置(index 0),null 值则可多个。

重写 equals 方法和 hashcode 方法

结论

        HashMap通过hash值和 equals 判断key是否相等,判断逻辑如下:

if (e.hash == hash &&
    ((k = e.key) == key || (key != null && key.equals(k))))

负载因子的选择

结论

        HashMap的默认负载因子(Load Factor)设为0.75,是基于‌空间利用率与时间效率的平衡‌,通过‌数学统计(泊松分布)‌和‌工程实践验证‌得出的最优值。同时,HashMap容量始终为2的幂,0.75可保证扩容阈值为整数。

采用(哈希码 & 数组长度减一)计算方式

结论

        HashMap使用(哈希码 & 数组长度减一)这种方式是一种优化技术,它利用了位运算来快速地将大范围的哈希码映射到较小范围的数组索引上,比模运算要快很多;HashMap 的容量设置为 2 的倍数,主要是为了通过位运算优化索引计算效率、减少扩容时的迁移成本,并提升哈希分布的均匀性。

<think>好的,我现在需要详细解释哈希映射(HashMap)的数据结构原理。首先,我得回想一下自己对HashMap的理解,并确保覆盖所有关键点。用户提供的引用内容中提到了哈希表作为底层结构、哈希函数、冲突处理等,这些都是需要涵盖的。 首先,HashMap的基本结构是什么?它基于哈希表,使用数组来存储键值对。哈希函数将键转换为数组的索引,这样可以直接访问对应位置的数据。这里要解释哈希函数的作用,比如将任意大小的数据映射到固定大小的值,作为数组下标。 接下来,哈希冲突是不可避免的,因为不同的键可能生成相同的哈希值。如何处理冲突?常见的方法有开放寻址法和链地址法。根据引用中的内容,用户可能已经提到过链地址法,即每个数组位置是一个链表,存放所有哈希冲突的键值对。需要详细说明这个过程,比如当发生冲突时,将新元素添加到链表中。 然后,需要讨论哈希函数的设计。好的哈希函数应该均匀分布键,以减少冲突。可能提到取模运算,比如用键的哈希值对数组长度取模得到索引。但数组的长度通常选择质数,以减少重复,比如引用中可能提到的。 扩容机制也是一个关键点。当元素数量超过负载因子(比如0.75)时,数组需要扩容,通常翻倍,并重新哈希所有元素到新的数组中。这会影响到性能,但能保持低冲突率。需要解释负载因子的作用和扩容的过程。 另外,时间复杂度方面,理想情况下,插入、删除、查找都是O(1),但最坏情况下(比如所有键冲突)退化为O(n)。所以强调良好的哈希函数和扩容策略的重要性。 可能还需要提到HashMap的应用场景,比如缓存、字典实现等,但用户主要问的是原理,这部分可以简要带过或者放在相关问题里。 现在检查是否有遗漏的部分。引用1提到C语言实现,引用2讲工作原理,引用3提到哈希表和HashMap的实现方法。所以需要综合这些信息,确保覆盖哈希表结构、哈希函数、冲突处理、扩容机制。 需要确保使用正确的中文术语,比如“链地址法”而不是“链表法”。另外,按照系统级指令,行内数学表达式用$...$,比如哈希函数h(key) = key % capacity,这里的公式需要用符号表示,可能需要用LaTeX格式。 在解释过程中,要自然引用提供的引用内容,比如在提到哈希表作为底层结构时引用[^1],在解释哈希函数时引用[^2],在冲突处理时引用[^3]。 最后,生成相关问题,这些问题应该与HashMap原理相关,比如哈希函数设计、冲突处理比较、扩容机制的影响、线程安全等,确保问题有针对性,帮助用户进一步学习。 总结思考步骤: 1. 定义和基本结构:哈希表、键值对存储。 2. 哈希函数的作用和设计。 3. 冲突处理方法,特别是链地址法。 4. 扩容机制和负载因子。 5. 时间复杂度分析。 6. 引用相关文献。 7. 生成相关问题。 现在将这些点组织成连贯的解释,确保符合用户的要求,结构清晰,分点说明,并正确使用格式。</think>### 哈希映射(HashMap)数据结构原理分步解释 #### 1. **基本结构** 哈希映射的核心是**哈希表**,它是一个数组结构,每个位置称为一个“桶”(Bucket)。每个桶存储键值对(Key-Value Pair)的集合。例如,一个容量为10的数组,索引为$0$到$9$,每个索引对应一个桶。 #### 2. **哈希函数** 哈希函数将键(Key)转换为数组索引,公式为: $$ \text{index} = h(\text{key}) \% \text{capacity} $$ 其中$h(\text{key})$是键的哈希值,$\text{capacity}$是数组容量。例如,若键为`"apple"`,哈希值为$1234$,容量为$10$,则索引为$1234 \% 10 = 4$。 **设计要求**: - **均匀分布**:减少冲突概率。 - **高效计算**:时间复杂度为$O(1)$。 #### 3. **冲突处理** 不同键可能产生相同的索引(冲突),常用**链地址法**解决: - 每个桶维护一个链表(或红黑树)。 - 冲突时,新键值对追加到链表末尾。 例如,键`"apple"`和`"banana"`同时映射到索引$4$,则链表存储这两个键值对[^3]。 #### 4. **扩容机制** 当元素数量与容量的比值(负载因子,默认$0.75$)超过阈值时,触发扩容: 1. 新建一个容量翻倍的数组。 2. 重新计算所有键的哈希值并分配到新桶。 此过程保证桶的负载降低,维持$O(1)$操作效率[^3]。 #### 5. **时间复杂度** - **理想情况**:插入、查找、删除均为$O(1)$(无冲突)。 - **最坏情况**:所有键冲突,退化为链表遍历$O(n)$。 优化手段包括使用红黑树(Java 8+)将链表操作优化至$O(\log n)$。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值