java 源码解析(01) HashMap

本文详细解析了HashMap的工作原理,包括其内部结构、构造方法、主要API操作流程及使用注意事项。

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

一、总述

    HashMap是基于散列表的Map的实现,提供了所有Map的操作接口,并支持使用null的keynull的value。HashMap是线程不安全的。

    由于散列表存在地址碰撞问题,那HashMap是采取什么方案解决这个问题的呢?使用的就是拉链法,但是拉链法使用链表存储,查找时存在效率比较低。如果Map容量太大而散列表容量太小,则碰撞几率很大,效率也就会急剧下降;相反,如果Map容量太小而散列表容量太大,则存在存储空间浪费。那怎么解决这对矛盾呢?HashMap使用动态散列表解决这个问题。动态散列表就是给散列表一个初始容量,当Map大小达到散列表容量的一定比例时,就扩充散列表容量,这个比例我们就叫它扩容因子

    由于使用拉链法,所以键值对的原始信息也需要存储起来,因此HashMap使用HashMapEntry这个结构体来记录每一个键值对,同时这个结构体也是链表的每一个项

源码:

    static class HashMapEntry<K, V>implements Entry<K, V> {
        final K key;    // key的值
        V value;    // value的值
        final int hash;    //key对应的hash
 
        /** hash一样但是key不一样的另一个键值对(同一hash地址的另一个键值对) **/
        HashMapEntry<K, V> next;
 
        HashMapEntry(K key, V value, int hash,HashMapEntry<K, V> next) {
            this.key = key;
            this.value = value;
            this.hash = hash;
            this.next = next;
        }
        // ... 其他代码
    }

所以HashMap的映射关系就变成了一个Key映射到一个HashMapEntry

  

二、HashMap的属性含义及作用

 

   /** 散列表最小容量 **/
    private static final int MINIMUM_CAPACITY =4;
 
    /** 散列表最大容量 **/
    private static final int MAXIMUM_CAPACITY =1 << 30;
 
    /** 空散列表,容量为0构造HashMap时,将使用这个散列表,当容量变化时该散列表将被替换 **/
    private static final Entry[] EMPTY_TABLE =new HashMapEntry[MINIMUM_CAPACITY >>> 1];
 
    /** 默认扩容因子 **/
    static final float DEFAULT_LOAD_FACTOR =.75F;
 
    /** 散列表存储数组。容量必须是2的指数(原因可能就是为了最优化散列表查找效率和空间利用率,具体算法我也没有看懂) **/
    transient HashMapEntry<K, V>[] table;
 
    /** 空Key对应的键值。由于空key是一个特殊的key,该key没有hashCode,无法在散列表中找到地址,所以专门为它开一个字段存储 **/
    transient HashMapEntry<K, V> entryForNullKey;
 
    /** 当前Map大小 **/
    transient int size;
 
    /** 记录修改次数。用于迭代操作校验 **/
    transient int modCount;
 
    /** 散列表扩容阀值,默认为散列表容量的0.75倍 **/
    private transient int threshold;
 
    /** 下面3个属性用于获取Map存储集合使用。他们都使用了延时初始化(使用时才初始化),
    * 他们实现是通过内部类实现,所以调用时能马上获取Map当前状态数据 **/
    private transient Set<K> keySet;
    private transient Set<Entry<K,V>> entrySet;
    private transient Collection<V>values;

 三、HashMap的构造方法

1、无参构造方法

散列表将是EMPTY_TABLE;扩容阀值为-1
源码:

    public HashMap() {
        table = (HashMapEntry<K, V>[])EMPTY_TABLE;
        threshold = -1;
    }

2、带初始容量的构造方法

构造步骤:
a、初始容量为负数将抛出IllegalArgumentException
b、初始容量为0等将同于无参构造方法
c、如果容量在最小容量到最大容量范围之外,则取临界值,到步骤e
d、其他情况(容量在最小容量到最大容量范围之间),将容量向上取到2的指数
e、调用makeTable 创建散列表
源码:

    public HashMap(int capacity) {
        if (capacity < 0) {
            throw newIllegalArgumentException("Capacity: " + capacity);
        }
        if (capacity == 0) {
           @SuppressWarnings("unchecked")
            HashMapEntry<K, V>[] tab =(HashMapEntry<K, V>[]) EMPTY_TABLE;
            table = tab;
            threshold = -1; // Forces firstput() to replace EMPTY_TABLE
            return;
        }
        if (capacity < MINIMUM_CAPACITY) {
            capacity = MINIMUM_CAPACITY;
        } else if (capacity >MAXIMUM_CAPACITY) {
            capacity = MAXIMUM_CAPACITY;
        } else {
            capacity =Collections.roundUpToPowerOfTwo(capacity);
        }
        makeTable(capacity);
    }

其中makeTable方法源码:

    private HashMapEntry<K, V>[]makeTable(int newCapacity) {
        HashMapEntry<K, V>[] newTable =(HashMapEntry<K, V>[]) new HashMapEntry[newCapacity];
        table = newTable;
        threshold = (newCapacity >> 1) +(newCapacity >> 2);  // 3/4capacity
        return newTable;
    }

3、 带初始容量和扩容因子的构造方法

该构造方法直接调用带初始容量的构造方法实现,只是对扩容因子进行校验而已。并没有使用到扩容因子,所以扩容因子还是默认的0.75
源码:

    public HashMap(int capacity, floatloadFactor) {
        this(capacity);
        if (loadFactor <= 0 ||Float.isNaN(loadFactor)) {
            throw newIllegalArgumentException("Load factor: " + loadFactor);
        }
    }

4、通过已经存在的Map构造

构造步骤:
a、根据源Map大小变换出一个初始容量,然后调用带初始容量的构造方法构造
b、调用constructorPutAll进行数据填充(除非散列表为EMPTY_TABLE,否则该方法不会检查容量,也就是不会自动扩容)
源码:

    public HashMap(Map<? extends K, ?extends V> map) {
        this(capacityForInitSize(map.size()));
        constructorPutAll(map);
    }

其中constructorPutAll源码如下:

    final void constructorPutAll(Map<?extends K, ? extends V> map) {
        if (table == EMPTY_TABLE) {
            doubleCapacity(); // Don't dounchecked puts to a shared table.
        }
        for (Entry<? extends K, ? extendsV> e : map.entrySet()) {
            constructorPut(e.getKey(),e.getValue());
        }
    }

四、HashMap的操作API

1、put方法

参数为:键值对;返回值为:该键对应的旧值,如果不存在旧值返回null
操作步骤:

a、如果key为空,则调用putValueForNullKey方法添加,然后结束
b、如果key不为空,则根据key计算出一个hash值,并与当前散列表容量计算得出一个索引值
c、根据索引值取出对应下标的键值对链表
d、然后依次对Key进行比较(先比较hash值,hash值一样再比较equals,equals也认为一样才是真正一样)
e、存在一样的就替换该键值对对应的value,然后将旧value值返回
f、如果不存在一样的key(索引对应链表不存在或者链表对应的key不存在),则更新修改次数标志和Map大小,检查Map大小是否达到扩容阀值
g、达到则进行扩容,刷新索引值;没达到跳过此步骤
h、调用addNewEntry对应索引值的位置增加一条链表(链表不存在)或者对该位置链表增加一个键值对到链表头(链表存在),返回null
源码:

    public V put(K key, V value) {
        if (key == null) {
            return putValueForNullKey(value);
        }
        int hash = secondaryHash(key);
        HashMapEntry<K, V>[] tab = table;
        int index = hash & (tab.length -1);
        for (HashMapEntry<K, V> e =tab[index]; e != null; e = e.next) {
            if (e.hash == hash &&key.equals(e.key)) {
                preModify(e);
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }
        modCount++;
        if (size++ > threshold) {
            tab = doubleCapacity();
            index = hash & (tab.length -1);
        }
        addNewEntry(key, value, hash, index);
        return null;
    }

其中addNewEntry源码:

    void addNewEntry(K key, V value, inthash, int index) {
        table[index] = new HashMapEntry<K,V>(key, value, hash, table[index]);
    }

扩容步骤:

a、如果当前容量达到最大容量,则不扩容直接返回
b、将容量扩大为原来的两倍,创建相应的散列表
c、遍历旧散列表的所有键值对,重新计算索引值,插入到新的散列表
d、返回新的散列表
代码:

    private HashMapEntry<K, V>[]doubleCapacity() {
        HashMapEntry<K, V>[] oldTable =table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            return oldTable;
        }
        int newCapacity = oldCapacity * 2;
        HashMapEntry<K, V>[] newTable =makeTable(newCapacity);
        if (size == 0) {
            return newTable;
        }
        for (int j = 0; j < oldCapacity;j++) {
            HashMapEntry<K, V> e =oldTable[j];
            if (e == null) {
                continue;
            }
            int highBit = e.hash &oldCapacity;
            HashMapEntry<K, V> broken =null;
            newTable[j | highBit] = e;
            for (HashMapEntry<K, V> n =e.next; n != null; e = n, n = n.next) {
                int nextHighBit = n.hash &oldCapacity;
                if (nextHighBit != highBit) {
                    if (broken == null)
                        newTable[j |nextHighBit] = n;
                    else
                        broken.next = n;
                    broken = e;
                    highBit = nextHighBit;
                }
            }
            if (broken != null)
                broken.next = null;
        }
        return newTable;
    }


2、putAll方法、remove方法、get方法、containsKey方法

这些方法都跟put类似,先根据key找到对应键值对,然后做相应操作


3、containsValue方法

参数为:value;返回值为:一个表示是否存在这样的一个键值对,它的值就是value的布尔值
操作步骤:
a、value为空,那就是循环散列表和散列表的每一条链表,查找是否存在值为空的键值对,存在就返回true
b、不存在就判断空key对应的键值对的value是否为空,然后返回
c、value不为空,那就是循环散列表和散列表的每一条链表,查找是否存在值通过equals方法判断一样的键值对,存在就返回true
d、不存在就判断空key对应的键值对的value通过equals方法判断是否一样一样,然后返回
源码如下:

    public boolean containsValue(Object value){
        HashMapEntry[] tab = table;
        int len = tab.length;
        if (value == null) {
            for (int i = 0; i < len; i++) {
                for (HashMapEntry e = tab[i]; e!= null; e = e.next) {
                    if (e.value == null) {
                        return true;
                    }
                }
            }
            return entryForNullKey != null&& entryForNullKey.value == null;
        }
        for (int i = 0; i < len; i++) {
            for (HashMapEntry e = tab[i]; e !=null; e = e.next) {
                if (value.equals(e.value)) {
                    return true;
                }
            }
        }
        return entryForNullKey != null&& value.equals(entryForNullKey.value);
    }
 

4、获取Map内部集合数据方法

由于这几个方法内部实现步骤基本一样,所以我就分析其中一个(keySet方法)
keySet方法源码很短,就是判断keySet属性是否存在值,存在就返回;否则就创建一个赋值到keySet属性并返回

    public Set<K> keySet() {
        Set<K> ks = keySet;
        return (ks != null) ? ks : (keySet =new KeySet());
    }

其中keySet属性没啥看的;现在我们就来看看KeySet这个类的实现。

源码:

    private final class KeySet extendsAbstractSet<K> {
        public Iterator<K> iterator() {
            return newKeyIterator();
        }
        public int size() {
            return size;
        }
        public boolean isEmpty() {
            return size == 0;
        }
        public boolean contains(Object o) {
            return containsKey(o);
        }
        public boolean remove(Object o) {
            int oldSize = size;
            HashMap.this.remove(o);
            return size != oldSize;
        }
        public void clear() {
            HashMap.this.clear();
        }
    }

这个类有以下特征:

a、这是一个实例内部类,继承了AbstractSet类
b、重写了一些方法,这些方法都是调用外部HashMap实例的相关属性和方法实现。
c、没有重写添加操作函数,调用添加函数将抛出UnsupportedOperationException异常(父类抛出)
这些方法中我们经常用到的就是迭代方法(iterator),接着我们就来看看这个方法
这个方法直接调用newKeyIterator方法返回,我们就来看看newKeyIterator方法。源码如下:

    Iterator<K> newKeyIterator() { returnnew KeyIterator(); }

直接创建一个KeyIterator迭代器返回,那我们再看看KeyIterator实现。源码如下:

    private final class KeyIteratorextends HashIterator implements Iterator<K> {
        public K next() { returnnextEntry().key; }
    }

这个类继承了HashIterator实现了Iterator,实现代码就一个方法(Iterator的next方法)

其中Iterator就是一个interface,没啥看的;我们就看看HashIterator。源码如下:

    private abstract class HashIterator {
        /**下一个键值对在散列表中的索引,初始值就是0 **/
        int nextIndex;
 
        /** 下一个键值对,初始值为为空key对应的键值对 **/
        HashMapEntry<K, V> nextEntry =entryForNullKey;
 
        /**最后一次返回的键值对,也就是当前迭代位置的键值对,删除时使用 **/
        HashMapEntry<K, V>lastEntryReturned;
 
        /**期望的修改值,用于校验迭代过程HashMap是否被外部修改,初始值为当前HashMap的修改值 **/
        int expectedModCount = modCount;
 
        HashIterator() {
            if (nextEntry == null) {
                HashMapEntry<K, V>[] tab= table;
                HashMapEntry<K, V> next =null;
                while (next == null &&nextIndex < tab.length) {
                    next = tab[nextIndex++];
                }
                nextEntry = next;
            }
        }
        public boolean hasNext() {
            return nextEntry != null;
        }
        HashMapEntry<K, V> nextEntry() {
            if (modCount != expectedModCount)
                throw newConcurrentModificationException();
            if (nextEntry == null)
                throw newNoSuchElementException();
            HashMapEntry<K, V>entryToReturn = nextEntry;
            HashMapEntry<K, V>[] tab = table;
            HashMapEntry<K, V> next =entryToReturn.next;
            while (next == null &&nextIndex < tab.length) {
                next = tab[nextIndex++];
            }
            nextEntry = next;
            return lastEntryReturned =entryToReturn;
        }
        public void remove() {
            if (lastEntryReturned == null)
                throw newIllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
           HashMap.this.remove(lastEntryReturned.key);
            lastEntryReturned = null;
            expectedModCount = modCount;
        }
    }

分析:

a、这个类是一个实例内部类,可以访问外部类的实例变量
b、构造函数就是查找下一个不为空的nextEntry,找不到就不做什么
c、hasNext方法就是判断nextEntry属性是否不为空
d、nextEntry 方法就是查找下一个不为空的nextEntry,并返回当前的nextEntry属性;
    查找之前先对修改值校验,所以调用这个方法的时候,外部不能就修改HashMap(并发下抛出ConcurrentModificationException的原因)
    也对当前的nextEntry属性进行校验,所以也要先调用hasNext判断是否存在下一个
e、remove方法就是删除当前迭代到的值
    删除之前对当前键值对进行非空判断,所以调用该方法之前要先调用nextEntry,并且知道当前的键值对不为空
    该方法也对修改值校验,所以调用这个方法的时候,外部不能就修改HashMap
    该方法删除了键值对之后重新更新了expectedModCount,所以下次的调用nextEntry不会引起抛出ConcurrentModificationException

 

五、HashMap使用细节

1、不允许存在相同的key(key的比较:先比较hashCode,再比较equal方法,两者都一样才是一样)
2、允许存在为空的key和value(最多只能存在一个key为空的键值对,后面的将会把前面的替换掉)
3、线程不安全,多线程操作需要同步或者使用Hashtable代替
4、使用迭代器获取下一个元素时,需要先判断是否还有下一个
5、使用迭代器迭代过程中不能通过HashMap自身操作函数修改HashMap
6、使用迭代器迭代过程中可以使用迭代器的remove删除元素;但对于同一个元素不能调用多次remove
7、扩容将进行大量遍历,所以如果知道大体容量,就使用带容量的构造参数构造

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值