Java集合类HashMap源码分析(JDK1.7)

点击查看Java集合类合集

HashMap概括

JDK7中HashMap的底层实现是哈希表(数组加链表实现)。当要向HashMap中添加新的元素时,先根据某种算法计算出key的hash值,并根据hash值和数组容量通过某种算法得出下标(&运算),当发生冲突时,将遍历相应位置的链表,如果key值重复,则将新的value覆盖老的value,并将老value值返回。如果key值不存在,则用头插法插入链表。key可以为null,此时将把元素的放到下标为0的链表中。当哈希表中数据个数超过阈值且,新元素要插入的位置已经存在元素时,将进行扩容。
Entry是哈希表中实际存储数据的对象,他是一个HashMap的静态类
HashMap存储数据不保证有序
(有些源码解释我以注解的形式写到了源码中)
示意图
在这里插入图片描述

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

        Entry(int var1, K var2, V var3, HashMap.Entry<K, V> var4) {
    
    
            this.value = var3;
            this.next = var4;
            this.key = var2;
            this.hash = var1;
        }

他有4个属性,分别是 key value hash 和 next指针

创建HashMap对象

public HashMap() {
    
    
        this(16, 0.75F);
    }
public HashMap(int var1) {
    
    
        this(var1, 0.75F);
    }

public HashMap(int var1, float var2) {
    
    
        this.table = (HashMap.Entry[])EMPTY_TABLE;
        this.hashSeed = 0;
        this.entrySet = null;
        if (var1 < 0) {
    
    
            throw new IllegalArgumentException("Illegal initial capacity: " + var1);
        } else {
    
    
            if (var1 > 1073741824) {
    
    
                var1 = 1073741824;
            }

            if (var2 > 0.0F && !Float.isNaN(var2)) {
    
    
                this.loadFactor = var2;
                this.threshold = var1;
                this.init();
            } else {
    
    
                throw new IllegalArgumentException("Illegal load factor: " + var2);
            }
        }
    }

创建HashMap对象时,可以传入两个参数,第一个参数val1为用户期望的哈希表数组容量(注意不是阈值,虽然在一开始会把这个值赋给阈值属性,但是添加属性时会改。也不宜一定是真正的数组容量),第二个参数val2为加载因子(用来计算阈值)。va1需要大于等于0.val2要大于等于0.

HashMap的数组容量需要为2的幂次数(如1 2 4 8 16)。将根据传入的参数计算出一个大于等于参数的2的幂次数作为数组大小。同时计算阈值(数组大小*加载因子)代码如下

private void inflateTable(int var1) {
    
    
        int var2 = roundUpToPowerOf2(var1);//获取数组大小
        this.threshold = (int)Math.min((float)var2 * this.loadFactor, 1.07374182E9F);//获取阈值
        this.table = new HashMap.Entry[var2];//初始化数组
        this.initHashSeedAsNeeded(var2);//设置hashSeed值(一般情况下均为默认值0)
    }
private static int roundUpToPowerOf2(int var0) {
    
    
        return var0 >= 1073741824 ? 1073741824 : (var0 > 1 ? Integer.highestOneBit(var0 - 1 << 1) : 1);
    }
public static int highestOneBit(int var0) {
    
    
        var0 |= var0 >> 1;
        var0 |= var0 >> 2;
        var0 |= var0 >> 4;
        var0 |= var0 >> 8;
        var0 |= var0 >> 16;
        return var0 - (var0 >>> 1);
    }

初始化HashMap

当第一次添加元素时,将进行哈希表的初始化

public V put(K var1, V var2) {
    
    
//如果是第一次添加元素,将进入这个if
        if (this.table == EMPTY_TABLE) {
    
    
            this.inflateTable(this.threshold);//初始化哈希表(代码见上面)
        }
		//key值为null时将把他放入下标为0的链表中,同时检测是否已经存在null值key
        if (var1 == null) {
    
    
            return this.putForNullKey(var2);
        } else {
    
    
        //根据哈希算法计算哈希值
            int var3 = this.hash(var1);
           // 根据哈希值和数组大小计算下标
            int var4 = indexFor(var3, this.table.length);
			//遍历相应数组下标的链表,判断key值是否重复,若重复则覆盖并返回旧value,若不重复则继续执行
            for(HashMap.Entry var5 = this.table[var4]; var5 != null; var5 = var5.next) {
    
    
                Object var6;
                if (var5.hash == var3 && ((var6 = var5.key) == var1 || var1.equals(var6))) {
    
    
                    Object var7 = var5.value;
                    var5.value = var2;
                    var5.recordAccess(this);
                    return var7;
                }
            }
			//对哈希表的操作计数(预防错误)
            ++this.modCount;
            //进行添加操作
            this.addEntry(var3, var1, var2, var4);
            return null;
        }
    }

哈希算法
将key的hashCode算法返回的哈希值进行右移操作,然后在进行亦或运算(让哈希值高位也参与到运算中来,防止因为哈希值高位发生变化而实际计算出的下标都相同的情况)

final int hash(Object var1) {
    
    
        int var2 = this.hashSeed;
        if (0 != var2 && var1 instanceof String) {
    
    
            return Hashing.stringHash32((String)var1);
        } else {
    
    
            var2 ^= var1.hashCode();
            var2 ^= var2 >>> 20 ^ var2 >>> 12;
            return var2 ^ var2 >>> 7 ^ var2 >>> 4;
        }
    }

计算数组下标算法
用哈希值与数组长度-1进行&运算。都到的值为【0,数组长度-1】

static int indexFor(int var0, int var1) {
    
    
        return var0 & var1 - 1;
    }

当key值不重复时,将进行头插法插入。此时需要进行扩容检测

void addEntry(int var1, K var2, V var3, int var4) {
    
    
		//当哈希表元素个数,大于等于阈值,并且,当前要添加位置不为null时,进行扩容。
        if (this.size >= this.threshold && null != this.table[var4]) {
    
    
            this.resize(2 * this.table.length);
            var1 = null != var2 ? this.hash(var2) : 0;
            var4 = indexFor(var1, this.table.length);
        }
	//真正的添加方法
        this.createEntry(var1, var2, var3, var4);
    }

添加元素方法

void createEntry(int var1, K var2, V var3, int var4) {
    
    
        HashMap.Entry var5 = this.table[var4];
        this.table[var4] = new HashMap.Entry(var1, var2, var3, var5);
        ++this.size;
    }

根据传入的哈希值 ,key ,value 数组下标来添加新元素。(头插)

扩容

this.resize(2 * this.table.length);//将当前数组扩大一倍

将阈值重新计算,并执行扩容

void resize(int var1) {
    
    
        HashMap.Entry[] var2 = this.table;
        int var3 = var2.length;
        if (var3 == 1073741824) {
    
    
            this.threshold = 2147483647;
        } else {
    
    
            HashMap.Entry[] var4 = new HashMap.Entry[var1];
            //将原来的数组复制到新的数组中,第二个参数为是否需要重新哈希
            //如果this.initHashSeedAsNeeded(var1)返回true则说明HashSeed可能改变了,因此需要重新计算哈希值
            this.transfer(var4, this.initHashSeedAsNeeded(var1));
            this.table = var4;
            this.threshold = (int)Math.min((float)var1 * this.loadFactor, 1.07374182E9F);
        }
    }
void transfer(HashMap.Entry[] var1, boolean var2) {
    
    
        int var3 = var1.length;
        HashMap.Entry[] var4 = this.table;
        int var5 = var4.length;

        HashMap.Entry var8;
        for(int var6 = 0; var6 < var5; ++var6) {
    
    
            for(HashMap.Entry var7 = var4[var6]; null != var7; var7 = var8) {
    
    
                var8 = var7.next;
                if (var2) {
    
    
                    var7.hash = null == var7.key ? 0 : this.hash(var7.key);
                }

                int var9 = indexFor(var7.hash, var3);
                var7.next = var1[var9];
                var1[var9] = var7;
            }
        }

    }

将原来的元素重新计算下标(或者重新计算哈希值再计算下标)。然后将entry对象放到新的数组中。

从HashMap中获取数据

public V get(Object var1) {
    
    
        if (var1 == null) {
    
    
        //key等于null时
            return this.getForNullKey();
        } else {
    
    
            HashMap.Entry var2 = this.getEntry(var1);
            return null == var2 ? null : var2.getValue();
        }
    }

如果key为nul,就去数组下标为0的位置寻找,当key等于null时,就返回。

private V getForNullKey() {
    
    
        if (this.size == 0) {
    
    
            return null;
        } else {
    
    
            for(HashMap.Entry var1 = this.table[0]; var1 != null; var1 = var1.next) {
    
    
                if (var1.key == null) {
    
    
                    return var1.value;
                }
            }

            return null;
        }
    }

如果key不为null,则根据计算出key的哈希值,根据哈希值和数组大小计算出下标。然后去相应下标处寻找。

final HashMap.Entry<K, V> getEntry(Object var1) {
    
    
        if (this.size == 0) {
    
    
            return null;
        } else {
    
    
        //计算哈希值
            int var2 = var1 == null ? 0 : this.hash(var1);

            for(HashMap.Entry var3 = this.table[indexFor(var2, this.table.length)]; var3 != null; var3 = var3.next) {
    
    
                Object var4;
                if (var3.hash == var2 && ((var4 = var3.key) == var1 || var1 != null && var1.equals(var4))) {
    
    
                    return var3;
                }
            }

            return null;
        }
    }

删除元素

根据key值删除哈希表中的元素。先将key值进行哈希,再利用哈希值和数组大小算出数组下标。然后遍历链表找到相应元素删除。

public V remove(Object var1) {
    
    
        HashMap.Entry var2 = this.removeEntryForKey(var1);
        return var2 == null ? null : var2.value;
    }
final HashMap.Entry<K, V> removeEntryForKey(Object var1) {
    
    
        if (this.size == 0) {
    
    
            return null;
        } else {
    
    
        //如果key为null
        //哈希值和数组下标均为0
            int var2 = var1 == null ? 0 : this.hash(var1);
            int var3 = indexFor(var2, this.table.length);
            HashMap.Entry var4 = this.table[var3];

            HashMap.Entry var5;
            HashMap.Entry var6;
            for(var5 = var4; var5 != null; var5 = var6) {
    
    
                var6 = var5.next;
                Object var7;
                if (var5.hash == var2 && ((var7 = var5.key) == var1 || var1 != null && var1.equals(var7))) {
    
    
                //操作量加1
                    ++this.modCount;
                //HashMap元素数减一
                    --this.size;
                    
                    if (var4 == var5) {
    
    
                        //如果头节点为要删除的元素
                        this.table[var3] = var6;
                    } else {
    
    
                    //如果中间节点为要删除的元素
                        var4.next = var6;
                    }
					//JDK1.7中为空方法
                    var5.recordRemoval(this);
                    return var5;
                }

                var4 = var5;
            }

            return var5;
        }
    }

HashMap线程不安全

HashMap是线程不安全的。
1.扩容时的并发问题
如果两个线程在给同一个HashMap赋值时,都发生了扩容。因为利用头插法扩展数组,所以在转移数据的时候,老数组的链表会逆序放入新数组(假设移动后下标都没变)。如果一个线程已经完成了转移,此时另一个线程的next对象引用和当前要转移的对象引用,的顺序就倒过来了,这个时候再进行转移,链表将会出现环。
2.遍历时的并发问题
当有两个线程一个进行读取一个进行修改时,也会产生并发问题。(modCount改变)

modCount的作用(快速失败)

当对HashMap进行增删操作时,均会使modCount递增。java规定在利用迭代器进行便利的时候应该保持容器的稳定,即不能在使用迭代器期间进行增删操作。而modeCount就是用来完成这件事的。当迭代器被创建的时候,将初始化this.expectedModCount = HashMap.this.modCount;。再利用迭代器进行操作时,会判断这两个量是否相等。如果在利用迭代器期间对容器进行了操作就会导致modCount递增,这时就和expectedModCount不当等,这时就会抛出异常throw new ConcurrentModificationException();

//创建迭代器
 private abstract class HashIterator<E> implements Iterator<E> {
    
    
        HashMap.Entry<K, V> next;
        int expectedModCount;
        int index;
        HashMap.Entry<K, V> current;

        HashIterator() {
    
    
        //记录modCount
            this.expectedModCount = HashMap.this.modCount;
            if (HashMap.this.size > 0) {
    
    
                HashMap.Entry[] var2 = HashMap.this.table;

                while(this.index < var2.length && (this.next = var2[this.index++]) == null) {
    
    
                }
            }

        }
//利用迭代器删除元素
public void remove() {
    
    
            if (this.current == null) {
    
    
                throw new IllegalStateException();
        		//判断是否相等(是否进行了增删)
            } else if (HashMap.this.modCount != this.expectedModCount) {
    
    
                throw new ConcurrentModificationException();
            } else {
    
    
                Object var1 = this.current.key;
                this.current = null;
                HashMap.this.removeEntryForKey(var1);
                this.expectedModCount = HashMap.this.modCount;
            }
        }
    }

例子

public class Test1 {
    
    
    public static void main(String[] args) {
    
    
        HashMap hashMap = new HashMap();
        hashMap.put("k1",1);
        hashMap.put("k2",2);
        hashMap.put("k3",2);
        Set set = hashMap.keySet();
        Iterator iterator = set.iterator();

        while(iterator.hasNext()) {
    
    
            Object o = iterator.next();
            System.out.println(o);
            set.remove("k1");
        }
    }
}

在这里插入图片描述

一个小疑惑

modCount是int类型。在java中int类型是4位32字节。如果对HashMap一直操作的话,要是他到了最大值之后怎么办呢?
——————————
经过测试,他变成最大之后,会从MIN_VALUE开始增加,这个检测依旧能正常进心。

猜你喜欢

转载自blog.csdn.net/qq_30033509/article/details/109219963