简析hashmap的实现原理

本文简要介绍了哈希表(HashMap)的基本概念,它是一种通过关键码值映射直接访问的数据结构,利用散列函数加快查找速度。HashMap在Java中通过数组结合链表的方式实现,当发生碰撞时,相同key的值会形成链表。在HashMap的初始化和put()、get()操作中,涉及到散列值计算和数组扩容等机制。

提一下哈希表,看下百科:
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
简单理解:1.通过某种算法(使用key的hash算法),计算出key的磁盘散列值,优点为速度和易用。
2.hashmap底层实现仍为数组(HashMap 底层就是一个数组结构,数组中的每一项又是一个链表。数组每个元素里存的是链表的表头信息,有了表头就可以遍历整个链表),当底层需要扩容,它会自动x2重新计算散列值,并把指针指向新的hashmap,而key相同的情况下,插入的value会形成一个链表
简析实现:
部分源码:

/**
     * Inflates the table.
     */
    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);

        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE table为Entry数组
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

初始化化hashmap时初始化一个Entry数组

put()方法:

/**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        //其允许存放null的key和null的value,当其key为null时,调用putForNullKey方法,放入到table[0]的这个位置
        if (key == null)
            return putForNullKey(value);
            //通过调用hash方法对key进行哈希,得到哈希之后的数值。该方法实现可以通过看源码,其目的是为了尽可能的让键值对可以分不到不同的桶中,个人理解为Entry
        int hash = hash(key);
        //根据indexFor计算出在数组中的位置
        int i = indexFor(hash, table.length);
        //如果i处的Entry不为null,则通过其next指针不断遍历e元素的下一个元素。
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //key.equals(k) 当完全匹配key值时
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                //替换当前新的value值,并实用链表结果进行数据储存,新加入的放在链头,最先加入的放在链尾
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        //如果hashmap中传入的key值不存在,则进行存储并返回null
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

get()

/**
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     *
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

/**
     * Offloaded version of get() to look up null keys.  Null keys map
     * to index 0.  This null case is split out into separate methods
     * for the sake of performance in the two most commonly used
     * operations (get and put), but incorporated with conditionals in
     * others.
     */
    private V getForNullKey() {
        //如果大小为0,则返回null
        if (size == 0) {
            return null;
        }
        //遍历获取null的值
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }
    注:hashmap既允许key为null,同时也允许value为null,而hashtable是禁止的。
HashMapJava 中非常重要的数据结构之一,广泛应用于各种需要快速查找的场景。其内部实现基于哈希表,通过键值对(Key-Value)的方式存储数据,并通过哈希函数将键映射到数组的索引位置,从而实现快速存取操作。 ### 哈希表的结构 HashMap 的内部结构主要由一个 `Node` 数组(称为 `table`)组成,每个 `Node` 对象代表一个键值对。当发生哈希冲突时,即不同的键映射到同一个索引位置,HashMap 采用链地址法来解决冲突。具体来说,每个 `Node` 可能是一个链表的头节点,该链表包含了所有映射到相同索引位置的键值对。 为了进一步优化性能,当链表长度超过一定阈值(默认为8)时,链表会转换为红黑树,从而将查找的时间复杂度从 O(n) 降低到 O(log n),显著提高了在大量数据情况下的性能表现[^2]。 ### 哈希函数与扰动函数 哈希函数的作用是将键(Key)转换为一个整数,这个整数再通过取模运算得到数组的索引。然而,直接使用键的哈希值可能会导致分布不均,尤其是在键的哈希值具有某些规律的情况下。为此,HashMap 引入了扰动函数(也称为哈希函数的一部分),通过对键的哈希值进行进一步处理,减少冲突的可能性,提高数据分布的均匀性[^1]。 ### 扩容机制 当 HashMap 中的元素数量超过一定阈值时,会触发扩容操作。这个阈值由负载因子(Load Factor)决定,默认情况下负载因子为 0.75,意味着当元素数量达到数组容量的 75% 时,HashMap 会进行扩容。扩容时,数组容量会加倍,同时所有元素会被重新哈希并插入到新的数组中。这一过程称为 rehashing,是 HashMap 性能优化的关键步骤之一[^4]。 在 JDK 1.7 和 JDK 1.8 中,扩容机制有所不同。JDK 1.8 对扩容进行了优化,使得在多线程环境下更加高效和安全。此外,扩容过程中还会根据链表长度决定是否将红黑树退化为链表,以节省内存空间[^4]。 ### 线程安全性 需要注意的是,HashMap 本身不是线程安全的。在多线程环境中,如果多个线程同时修改 HashMap,可能会导致数据不一致或其他异常情况。因此,在并发环境下,建议使用 `ConcurrentHashMap` 或者通过外部加锁的方式来保证线程安全。 ### 示例代码 以下是一个简单的 HashMap 使用示例: ```java import java.util.HashMap; public class HashMapExample { public static void main(String[] args) { HashMap<String, Integer> map = new HashMap<>(); map.put("one", 1); map.put("two", 2); map.put("three", 3); System.out.println("Value for key 'two': " + map.get("two")); // 输出: Value for key 'two': 2 System.out.println("Size of map: " + map.size()); // 输出: Size of map: 3 } } ``` ### 相关问题 1. HashMap 在什么情况下会触发扩容操作? 2. 如何理解 HashMap 中的负载因子? 3. 为什么 HashMap 的初始容量必须是 2 的幂次方? 4. HashMap 和 TreeMap 有什么区别? 5. 在多线程环境下,为什么推荐使用 ConcurrentHashMap 而不是 HashMap
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值