HashMap原理与实现

一、什么是HashMap

HashMap是InterfaceMap的实现类,HashMap底层采用了哈希表,它是一种十分重要的数据结构。
数据结构中使用数组和链表对数据进行存储,他们各有特点:
数组:索引效率高,但插入,删除元素效率低。
链表:插入,删除元素效率高,但是索引效率低。
而哈希表结合了数组与链表的优点,具有索引效率高,插入,删除元素也十分方便的特点。他的本质就是“数组加链表”。

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

二、类属性

在这里插入图片描述
HashMap共有十三个类属性,除serialVersionUID外分别是

1、DEFAULT_INITIAL_CAPACITY = 1 << 4

默认初始容量,HashMap的容量必须是2的幂;

2、threshold

下一次扩容临界值,new HashMap时,threshold为当前HashMap最大容量,调用resize()初始化或扩容table后,threshold为 当前HashMap最大容量*负载因子

3、MAXIMUM_CAPACITY = 1 << 30

HashMap最大容量,如果需求的容量大于最大容量,也只会构造1073741824即0100 0000*7 位的HashMap;

4、DEFAULT_LOAD_FACTOR = 0.75f

默认负载因子,当HashMap 存入的数目 >=(负载因子 * 当前容器容量) 时HashMap会进行扩容操作;

5、loadFactor

实际负载因子

6、TREEIFY_THRESHOLD = 8

当数组(table属性)某一位存储的数据条目数大于此值,数组中该位的存储方式将从链表替换为红黑树。该值必须大于2,并且至少应为8,以符合树移除中关于收缩后转换回普通存储方式的假设;

7、UNTREEIFY_THRESHOLD = 6

树结构还原链表阈值,最大为6;

8、MIN_TREEIFY_CAPACITY = 64

哈希表的最小树形化容量。当哈希表中的容量大于这个值时,表中的桶才能进行树形化否则桶内元素太多时会扩容,而不是树形化为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD

9、table

数据类型为HashMap.Node<K,V>[],HashMap数据存储数组,大小为2的倍数,根据对象的hashcode,用数组长度进行取余为数组下标,存入或读取table,如:java.util.HashMap#putVal:630。
==new HashMap后,table并未第一时间初始化,只有涉及到存入数据时才会进行table的初始化。具体代码见 ==
数组长度要为2的幂,才能支持这样的取余,至于为什么使用与运算来取余而不是模运算,我查阅网上资料找到的是,使用模运算涉及到除运算,效率是远低于位运算的。
(因为我也很菜,一开始不能理解此处与运算,但自己手算后发现是极易理解的,后面会写上我对于这点的论证)

hash(key)&(table.length - 1)
// 查阅网上资料,jdk8之前hash方法返回的是key的hashcode,由于本地没有8之前的jdk故未进行查阅
hash(key) == key.hashcode();

取余的目的是想使数据能够更均匀的存入table中,减少碰撞。至于为什么更均匀,我暂时也不清楚,希望之后能通过jvm源码关于hashcode的生成找到答案。
同时,这也是为什么table不是一个单纯的数组,而是一个链表数组,为了防止hash(key)&(table.length - 1)取数组下标出现碰撞,当碰撞发生,遍历该链表,比对hash与key,存在则根据配置是否替换,不存在则生成一个新的节点加入链表
链表的扩容判断与链表数组是否存在为空数据无关java.util.HashMap.java#putVal:662

// HashMap.Node 数据结构
static class Node<K,V> implements Map.Entry<K,V> {
		/** hash(key)的返回值 */
        final int hash;
        /** 键 */
        final K key; 
        /** 值 */
        V value;
        /** 下一个节点 */
        Node<K,V> next;
        
       ...... 构造方法和类方法省略
}

jdk8中,获取hashcode加入了扰动函数

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

扰动函数的前提:java.lang.Object#hashCode方法返回的是int类型hashcode,32位;
扰动函数的目的:为了混合原始hashcode的高位和地位,以此来加大低位的随机性。从而更大程度的减少取余后碰撞,即避免出现某个数组下标对应的数据集相比其他下标对应的数据集大得多;
网上查阅使用此扰动函数,只找到提及《An introduction to optimising a hashing strategy》中实验随机选取了352个字符串,在他们散列值完全没有冲突的前提下,对它们做低位掩码,取数组下标当HashMap数组长度为512的时候,也就是用掩码取低9位的时候,在没有扰动函数的情况下,发生了103次碰撞,接近30%。而在使用了扰动函数之后只有92次碰撞。碰撞减少了将近10%。看来扰动函数确实还是有功效的。

10、modCount

对该HashMap进行结构修改的次数
结构修改是指更改HashMap中的映射数或以其他方式修改其内部结构(例如,重新哈希)的修改。 此字段用于使HashMap的Collection-view上的迭代器快速失败。
修改已存在的key对应的value不会增加此计数,仅新增、删除节点会增加此计数。
以下方法存在++modCount:
1、java.util.HashMap#putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
2、java.util.HashMap#removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)
3、java.util.HashMap#computeIfAbsent(key, Function)
判断key是否存在,
若key存在且value不为空,则返回value。
若key不存在 或 key存在但Node.value为空,进入入参实现的java.util.function.Function#apply方法,若apply方法返回null,则返回null。若apply返回不为null,当key存在但Node.value为空则将apply返回值赋值给Node.value,不修改modCount返回apply执行结果;当key不存在则新建一个链表节点Node,新建节点会++modCount再返回apply执行结果。

4、java.util.HashMap#compute(key, BiFunction)
判断key是否存在,
进入入参实现的java.util.function.BiFunction#apply方法,若apply方法返回null且key存在,则删除该key节点,若apply返回不为null,则修改该key节点的value值。删除节点调用了removeNode
若apply方法返回不为null,新建节点,然后++modCount再返回apply执行结果。

5、java.util.HashMap#merge(key, value, BiFunction)
判断key是否存在,
若key存在,且key对应的Node.value不为空,则进入入参实现的java.util.function.BiFunction#apply方法,则最终用于操作的v为apply返回值;Node.value为空的话,v则为入参value。若v==null则调用removeNode;若v!=null则修改Node.value,不修改modCount。
若key不存在,则新建节点,然后++modCount再返回apply执行结果。

11、entrySet

This map的Set集合映射,数据类型为Set<Map.Entry<K,V>>,保存缓存的java.util.HashMap#entrySet,单例模式。
具体数据类型为java.util.HashMap.EntrySet,该Set与This map是双向绑定的。
Map.Entry<K,V>是Map中的接口,在HashMap中的使用应该是HashMap.Node<K, V>。
EntrySet中几个方法
java.util.HashMap.EntrySet#size 返回Map中元素数量
java.util.HashMap.EntrySet#clear 清空Map
java.util.HashMap.EntrySet#contains 判断键值对是否存在此Map中,Key和Value都要对应
java.util.HashMap.EntrySet#remove 移除键值对,Key和Value都要对应
java.util.HashMap.EntrySet#spliterator 返回一个继承于HashMapSpliterator的EntrySpliterator迭代器,之后看单独写一篇放链接还是写下面
java.util.HashMap.EntrySet#forEach jdk8新增的遍历接口,传入一个实现java.util.function.Consumer的类,遍历出Node传入java.util.function.Consumer#accept执行
以上接口对应源码很好理解

主要看一下java.util.HashMap.EntrySet#iterator方法,该方法用于获取一个Iterator<Map.Entry<K,V>>迭代器,具体实现类型为java.util.HashMap.EntryIterator继承于java.util.HashMap.HashIterator,迭代器的原理按照代码很容易理解,我觉得关注点在于对于HashMap的增删操作都会使得已生成的迭代器失效

final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> {
    public final Map.Entry<K,V> next() { return nextNode(); }
}

abstract class HashIterator {
        Node<K,V> next;        // next entry to return
        Node<K,V> current;     // current entry
        int expectedModCount;  // for fast-fail 判断Map结构是否改变
        int index;             // current slot

        HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                // 找到第一个不为空的链表
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

        public final boolean hasNext() {return next != null;}

        final Node<K,V> nextNode() {
            Node<K,V>[] t;
            Node<K,V> e = next;
            if (modCount != expectedModCount) 
            	throw new ConcurrentModificationException();
            if (e == null) 
            	throw new NoSuchElementException();
            if ((next = (current = e).next) == null && (t = table) != null) {
                do {} while (index < t.length && (next = t[index++]) == null);
            }
            return e;
        }

        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }

12、size

HashMap当前存储的键值对数量

三、HashMap的构造方法

java.util.HashMap#HashMap(int)
java.util.HashMap#HashMap()
上面两个不看,是使用默认属性构造HashMap(int, float)
java.util.HashMap#HashMap(int, float)
java.util.HashMap#HashMap(java.util.Map<? extends K,? extends V>)

1、HashMap(int, float)

常用的构造方法,仅是初始化一个HashMap,有必要一看的是tableSizeFor方法,在类属性DEFAULT_INITIAL_CAPACITY

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

值得一看的是tableSizeFor(),HashMap的初始化会计算转换容量为2幂,且小于等于最大值

	// java.util.HashMap#HashMap:457 调用此方法计算下一次扩容临界值threshold
	// new过程未加入负载因子计算,调用resize()加入负载因子计算
	// cap 传入的初始容量
	static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

此处逻辑很好理解

初始容量最高位肯定为1,且int类型8字节32位,经过五次移位后或计算,最高的1肯定会填满低位的0
int cap      = 0010 0000 0000 0000 0000 0000 0000 0001
cap - 1      = 0010 0000 0000 0000 0000 0000 0000 0000
n |= n >>> 1 = 0011 0000 0000 0000 0000 0000 0000 0000
n |= n >>> 2 = 0011 1100 0000 0000 0000 0000 0000 0000
n |= n >>> 4 = 0011 1111 1100 0000 0000 0000 0000 0000
n |= n >>> 8 = 0011 1111 1111 1111 1100 0000 0000 0000
n |= n >>>16 = 0011 1111 1111 1111 1111 1111 1111 1111
n + 1        = 0100 0000 0000 0000 0000 0000 0000 0000

2、HashMap(java.util.Map<? extends K,? extends V>)

使用一个Map接口实现类构造一个新的HashMap
主要调用的是putMapEntries方法

    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
    
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                float ft = ((float)s / this.loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
                if (t > this.threshold)
                    this.threshold = tableSizeFor(t);
            }
            else if (s > this.threshold)
            	// 当调用HashMap.putAll时,threshold已有初始值,此时比较容量不足,进行扩容
                resize();
            // 使用Map的Set映射遍历存入数据
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

四、HashMap的数据存储

HashMap数据存储结构
HashMap数据存储结构
数据存储方法:
java.util.HashMap#put(K key, V value)
java.util.HashMap#putAll(Map<? extends K, ? extends V> m)
java.util.HashMap#putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
putAll调用的是putMapEntries,上面有讲解,put调用的是putVal

链表格式转换的红黑树格式建议单独学习了解,可以先了解平衡二叉树的机制(平衡二叉树的原理与Java实现),再学习红黑二叉树(红黑树解读与Java实现),理解了红黑二叉树之后,对于HashMap的增删改查也就不会有任何疑问了。

	/**
	 * hash – 也就是HashMap.hash(key)
	 * key
	 * value
	 * onlyIfAbsent – 如果为true,将不修改已存在的键值对
	 * evict – 官方注释是如果为false,说明table处于创建模式,但是未看到对此方法有何影响
	 */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
        	// 如果table为空,初始化table
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
        	// 根据对key的hash取余操作,得到该key在table中的数组下标
        	// 为空说明没有碰撞,直接存入
            tab[i] = newNode(hash, key, value, null);
        else {
        	// 非空则说明发生了碰撞,需要去该下标对应的链表确认是否key已存在
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                // key已存在且在链表首位,e即为该key对应的原始Node
                e = p;
            else if (p instanceof TreeNode)
            	// 如果该链表已转换为红黑树,通过红黑树遍历寻找该key对应值
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            	// key已存在但不在链表首位
                for (int binCount = 0; ; ++binCount) {
                	// 遍历链表
                    if ((e = p.next) == null) {
                    	// 第一个if条件中已经判断非首位key对应,判断下一位Node e是否为空
                    	// e为空说明该key不存在于此HashMap
                        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))))
                        // e不为空,判断当前e.key是否是要存入的key,是的话提取出e
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
            	// e不为空,说明存在此key的键值对,根据onlyIfAbsent判断是否修改value
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 新增节点使modCount改变
        ++modCount;
        if (++size > threshold)
        	// 存入的总数据条目数大于扩容临界值则构建新的扩容的table
            resize();
        afterNodeInsertion(evict);
        return null;
    }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HashMap是基于hashing的原理实现的。当我们使用put(key, value)方法将对象存储到HashMap中时,首先会对键调用hashCode()方法,计算并返回的hashCode用于找到Map数组的bucket位置来存储Node对象。HashMap使用数组和链表的数据结构,即散列桶,来存储键值对映射。HashMap的工作原理是通过计算键的hashCode来确定存储位置,并使用链表解决哈希冲突。当多个键具有相同的hashCode时,它们会被存储在同一个bucket中的链表中。当我们使用get(key)方法从HashMap中获取对象时,会根据键的hashCode找到对应的bucket,然后遍历链表找到对应的值对象。HashMap实现基于一个线性数组,即Entry\[\],其中保存了键值对的信息。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *2* [javaHashMap原理](https://blog.csdn.net/songhuanfeng/article/details/93905015)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [HashMap实现原理分析](https://blog.csdn.net/vking_wang/article/details/14166593)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值