Java7 中的 HashMap 的put(),get()简单解析,remove()方法中保留的一个不是很复杂的问题

本文介绍了Java7 HashMap的重要概念,如容量、负载因子和扩容阈值。详细讲解了put()方法如何处理键值对,包括Entry类的结构和覆盖原有键值对的情况。get()方法的实现通过获取Entry来查找键对应的值。讨论了remove()方法,特别是其内部的removeEntryForKey()方法,提出了关于在遍历链表过程中减少size的疑问,引发了是否可能存在的潜在问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. HashMap 关键名词:  16和0.75 是设计者结合空间和时间考虑的;

     1. capacity     :  当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍;

     2. loadFactor :负载因子,默认为 0.75;

     3. threshold   :扩容的阈值,或者叫扩容临界值,等于 capacity * loadFactor;

     4. 一个数组,数组中每个元素组成一个单向链表。;

单向链表关键名词:

     Entry 类,包含四个属性:key, value, hash 值, next指向单向链表的下一个节点。

1.1 Entry 类:

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

 

2. put()方法简单介绍:当在链表中已经存在相同的hash和key时,覆盖原值,并将原值返回;

public V put(K key, V value) {
    // 当插入第一个元素的时候,需要先初始化数组大小,即hashmap的大小;
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 如果 key 为 null,会将 entry 放到 table[0] 的位置
    if (key == null)
        return putForNullKey(value);
    // 1. 对 key 求 hash 值
    int hash = hash(key);
    // 2. 找到hash值对应的数组下标
    int i = indexFor(hash, table.length);
    // 3. 遍历 数组中对应下标处的链表,比较 key 如果有重复,直接覆盖,put 方法返回旧值
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    //hashmap在结构上被修改的次数(暂时不解释)
    modCount++;
    // 4. key 不重复 ,将此 entry 添加到链表中
    addEntry(hash, key, value, i);
    return null;
}

 2.1. inflateTable(threshold);方法简介new HashMap<>() 事实上基本什么也没有做,默认扩容的阈值为16;

/**
     * Inflates the table. 创建数组空间
     */
    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize 必须是2的N次幂;
        int capacity = roundUpToPowerOf2(toSize);
        // 计算扩容阈值:capacity * loadFactor
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        // 初始化数组
        table = new Entry[capacity];
        //此方法不看也罢
        initHashSeedAsNeeded(capacity);
    }

2.2.addEntry(int hash, K key, V value, int bucketIndex) :四个参数:key的hash值,key值,value值,数组下标值;

    //添加Entry的过程
    void addEntry(int hash, K key, V value, int bucketIndex) {
        //如果当前数组的大小达到或者超过扩容的阈值;且新值要插入的数组位置已有元素则需要扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //扩容按当前数组的2倍长度值处理
            resize(2 * table.length);
            //从新技术hash值
            hash = (null != key) ? hash(key) : 0;
            //计算hash值在新数组中的下标地址
            bucketIndex = indexFor(hash, table.length);
        }
        //将新值放到链表的表头,然后 size++
        createEntry(hash, key, value, bucketIndex);
    }


   
    //利用下标获取对应的数组
    void createEntry(int hash, K key, V value, int bucketIndex) {
        //利用下标获取对应的数组
        Entry<K,V> e = table[bucketIndex];
        //将新值放到链表的表头(这里的代码是将新建的Entry 给到数组,所以在放在链表的表头位置)
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        //数组的大小加一
        size++;
    }

2.3.resize(int newCapacity):扩容按当前数组的2倍长度值处理

    void resize(int newCapacity) {
        //获取老的数组,以及原来的长度
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        //建立一个二倍于老数组的新数组
        Entry[] newTable = new Entry[newCapacity];
        //将老数组中的数据节点转换到新的数组中
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        //扩容结束,全局变量赋值
        table = newTable;
        //重置扩容阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

2.4.transfer(Entry[] newTable, boolean rehash):数据迁移

    // 由于是双倍扩容,迁移过程中,会将原来 table[i] 中的链表的所有节点,分拆到新的数组的 newTable[i]
    // 和 newTable[i + oldLength] 位置上。如原来数组长度是 16,那么扩容后,原来 table[0] 处的链表中
    // 的所有元素会被分配到新数组中 newTable[0] 和 newTable[16] 这两个位置
 void transfer(Entry[] newTable, boolean rehash) {
        //新数组的大小
        int newCapacity = newTable.length;
        //将老数组中,每个单向链表上Entry全部读出来
        for (Entry<K,V> e : table) {
            while(null != e) {
                //每个不为null的节点 
                Entry<K,V> next = e.next;
                if (rehash) {
                    //重新计算 key 的hash值
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //算出新数字的下标位置,放入新的数组中
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

3.get()方法简介:

public V get(Object key) {
        if (key == null)
            //key为null,从数组table[0]中获取key==null的entry中的value
            return getForNullKey();
        //通过key,获取entry 
        Entry<K,V> entry = getEntry(key);
        //entry 不为null,返回entry中的value
        return null == entry ? null : entry.getValue();
    }

3.1 Entry<K,V> getEntry(Object key):获取Entry

    final Entry<K,V> getEntry(Object key) {
        //数组大小为0,返回null
        if (size == 0) {
            return null;
        }
        //计算key的hash值
        int hash = (key == null) ? 0 : hash(key);
        //获取对应数组上的落表
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            //hash值相同,且 key相同的情况下,返回对应Entry
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        //在链表中没有找到,返回null
        return null;
    }

4.V remove(Object key):删除单个key,注意返回是的K-V中的Value;

public V remove(Object key) {
        Entry<K,V> e = removeEntryForKey(key);
        return (e == null ? null : e.value);
    }

4.1 final Entry<K,V> removeEntryForKey(Object key) :


    final Entry<K,V> removeEntryForKey(Object key) {
        if (size == 0) {
            return null;
        }
        //计算hash
        int hash = (key == null) ? 0 : hash(key);
        //获取数组下标
        int i = indexFor(hash, table.length);
        //在数组中的位置table[i],即链表的首节点做一次赋值
        Entry<K,V> prev = table[i];
        //该行代码没有具体含义
        Entry<K,V> e = prev;
        //遍历链表
        while (e != null) {
            //next 可以是null
            Entry<K,V> next = e.next;
            Object k;
            //比较hash值和key值
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k)))) {
                //hashmap在结构上被修改的次数(暂时不解释)
                modCount++;
                //匹配到了对应节点,数组的大小减一
                size--;
                //如果要删除的节点是首节点,则将链表中的下一个节点设置为首节点
                if (prev == e)
                    table[i] = next;
                else
                    //如果首节点不是要删除的节点,将首节点prev的next存e的next节点
                    prev.next = next;
                e.recordRemoval(this);
                return e;
            }
            prev = e;
            e = next;
        }

        return e;
    }

removeEntryForKey()提一个问题:while (e != null) {} 遍历链表,假设链表的长度为3,如果比较hash值和key值相同确认,一个entry 后,比如是第二个,那为什么此时可以直接做size--;这不是bug?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值