java——深入理解HashMap

mac2026-05-08  2

HashMap

前沿:首先HashMap具体实现是由链表+数组的实现方式,并且采用了动态扩容技术

主要对象

table

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; 注意是transient,非序列化 1.数组还有很多的空间没有被使用,没有被使用到的空间被序列化没有意义 2.不同的虚拟机对于相同 hashCode 产生的 Code 值可能是不一样的

Entry

Entry<K,V>是链表和数组的主要组成: 其重要的成员变量是Key,Value,Entry<K,V>next,hash

static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构 int hash; Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } ```

构造如下: 其他组成: transient int size;//实际个数 int capacity;//实际大小 int initialCapacity,//初始的数组大小 int threshold;//capacity*loadFactory。HashMap put新元素后,如果size>threhold,需要扩容

/**负载因子,代表了table的填充度有多少,默认是0.75 加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。 */ final float loadFactor;

/*HashMap被改变的次数,由于HashMap非线程安全,在对HashMap进行迭代时,如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),需要抛出异常ConcurrentModificationException/ transient int modCount;

HashMap方法:

1:构造方法 //主要是对传入的initialCapacity和loadFactor进行参数检验,没有为数组table分配内存空间而是在执行put操作的时候才真正构建table数组

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; threshold = initialCapacity;     init();//init方法在HashMap中没有实际实现,不过在其子类如 linkedHashMap中就会有对应实现 }

2:put 主要分为四部分: 1:如果table为空,需要创建table 2:计算机hash值和数组的index 3:如果在数组中存在,则替换并返回旧hash 4:如果在数组中不存在,则直接插入数组

public V put(K key, V value) { //如果table数组为空数组{},进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4(16)==后面会改变== if (table == EMPTY_TABLE) { inflateTable(threshold); } //如果key为null,存储位置为table[0]或table[0]的冲突链上 if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length);//获取在table中的实际位置 for (Entry<K,V> e = table[i]; e != null; e = e.next) { //如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value 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; } } modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败 //如果该对应数据不存在,直接加入 addEntry(hash, key, value, i);//新增一个entry return null; }

其中使用的方法: inflateTable(),hash(),indexFor(),recordAccess(),addEntry()

hash():转为hash值 addEntry():加入hash数组,并且根据情况进行扩容

如何初始扩容:

private void inflateTable(int toSize) { int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次幂 /**此处为threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值, capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1 */ threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; initHashSeedAsNeeded(capacity); }

如何根据hash确定索引:

static int indexFor(int h, int length) { return h & (length-1); }

关键点:为什么实际大小总是2的幂? 我的理解: 原因一: 因为index=hash%length-1 比如对于: length=16 则length-1 =15 即 00001111 length=32 则length-1=31 即 00011111 可以看到,对于一个相同的hash,分别&两个不同的length-1,得到的结果result1,result2 差别就只在右数第5位,相当于扩容后只要修改一位就可以改变索引

原因二: 因为length=2^n,则length-1的位都是保持00001111111的形状,而hash&length-1中,高位不会产生影响,而低位任意一个变化变化都会产生影响,减少冲突的概率。 在h<length的情况下: 对于长度:23 length-1则比特为 10110,则对于结果result=10110 既可以是h=11110 10110 11111 10111 都对应相同的结果冲突增加 而对于长度32 length-1 则比特位11111 ,则对于结果10110 只有h=10110才可以与其对应

其实HashMap还要其他方法,但是感觉关键部分就是上面写的,如果还想加深了解,可以看一下

最新回复(0)