HashMap1.7原理浅析

HashMap大家通常喜欢拿1.7和1.8进行比较,虽然现在基本都升级成java8了,但是有时候会聊到,有些原理会搞不太清楚,那么就来确认下1.7的各个参数的含义以及怎么进行插入和扩容的

对于HashMap的大致理解

HashMap是由数组加链表组成的,插入元素的时候会先进行计算key的hash值,然后计算对应的数组的下标,找到对应位置,如果没有元素,直接放入数组位置,如果当前位置存在元素,那么把当前节点的下一个节点指向原来的节点,然后进行插入(头插法),如果存在对应的元素,应该替换原来的元素,对于查找的时候也是先计算hash值,找到对应的下标,然后进行对链表的遍历比较,如果有匹配的就进行返回,无返回null
HashMap的扩容 当超过阈值的时候HashMap进行扩容,会申请原来两倍长的数组,然后对节点进行转移,HashMap是线程不安全的,当多线程转移的时候数据可能出错,著名的就是HashMap1.7的扩容死循环问题,因为头插法导致的,再次获取到这个位置Hash的时候就会死循环
大致理解的就是这么多了吗,那么如果再深入问下细节呢,比如HashMap的默认长度,扩容因子是什么,为什么是这么大,HashMap究竟什么时间扩容,是到了扩容数量就扩容吗,是怎么进行扩容的,数据是怎么进行转移的,为什么头插法会产生死循环,什么情况下发生,扩容会进行rehash吗,对象可以作为key吗,如果作为key会有什么问题,怎么解决,为什么会这样?
如果这样细节性追问,容易问到一些不太记得详细的,下面来深入研究下,HashMap1.7的具体执行流程,主要原理。

HashMap的插入

先从HashMap的插入开始看吧,即 put()方法

    public V put(K key, V value) {
        // 如果数组还没初始化,先初始化数组,初始化的具体可以放后面再仔细看下
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        // 对于空值的处理,直接放tab[0]了
        if (key == null)
            return putForNullKey(value);
        // hash 对象的hash,然后|右移16位,即常说的高16位|低16位
        int hash = hash(key);
        // 计算需要存放的位置,直接&数组长度得到一个下标
        int i = indexFor(hash, table.length);
        // 如果位置不为空
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            // 遍历找到了相同的key进行替换,然后返回旧值
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
    	// 走到这就是没替换到相同的key
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

put 方法简单总结,就是数组未初始化的先进行初始化,然后key为null的直接放到0下标位置,不为null的取hash值,然后根据数组长度得到需要存放位置,然后查看是否有相同的key,有则替换,无则走到下面直接添加,那么接下来分析下初始化数组过程,求hash过程,最后就是addEntry 是怎么进行添加的

初始化map数组过程

    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
    	// 计算最终的capacity,如果无参数构造的map就是16,如果填入的参数,比如5,那么就
    	// 进行计算得到的是大于等于这个值的最近的一个2的n次幂
        int capacity = roundUpToPowerOf2(toSize);
    	// 计算得到的阈值,这个的capacity * loadFactor 都可以自定义传入,(但是容量如果不是2的n次幂会被转换下)
    	// 然后对比下最大值
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        // 然后创建对应长度的数组
		table = new Entry[capacity];
    	// 初始化hash值掩码,即参与hash的生成规则,暂时不分析这个
        initHashSeedAsNeeded(capacity);
    }


    private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
    	// 对给到的数字进行校验,超过最大值就默认最大值,小于等于1就默认1,
    	// 剩下的就走的先把数字 减去1,然后左移动1位(等于*2),这样是为了保证得到一个
    	// 大于等于的这个数字的2的n次幂,highestOneBit 是为了得到最高位代表的数据
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

初始化map的数组就比较简单,根据容量和扩容因子计算扩容阈值,这里向下取整的,即如果传入的capacity为4,loadFactor为0.8,得到的就是3,创建的是长度为4的数组,计算数组的长度的也算一个点,得到大于等于这个值的2的n次幂,通过-1,然后左移动一位,然后取最高位代表的值,这样得到的就是大于等于源数据的2的n次幂(这里把1除了,因为1直接得1)

hash过程简单分析

    final int hash(Object k) {
        // 参与运算的掩码
        int h = hashSeed;
    	// 这里直接计算string的的hash,暂不追
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
    	// 这里掩码和对象的hashCode进行异或运算
        h ^= k.hashCode();

        //分别将输入值向右移动20和12位。然后,通过对这两个结果进行异或操作,
	//将它们与原始值进行混合。接着,再将结果右移7位、4位,并再次与原始值进行异或操作。
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

这里的hash主要就是有个自己的掩码参与运算,然后后面对数据进行向右移动几次位置进行异或,为了打乱hashcode值,使其更加分散,受到某些因素影响较小

addEntry是怎么进行插入节点的

    void addEntry(int hash, K key, V value, int bucketIndex) {
        // 先判断下是不是超过阈值,并且这次插入hash冲突,如果不冲突,可以先不扩容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            // 扩容为原来长度的2倍
            resize(2 * table.length);
            // 这里扩容之后重新进行了hash
            hash = (null != key) ? hash(key) : 0;
            // 确定下下标
            bucketIndex = indexFor(hash, table.length);
        }

        createEntry(hash, key, value, bucketIndex);
    }

    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        // 这里看起来直接一个entry到对应位置了,实际在Entry的构造方法中
        // 把next指针指向了原来的节点,这就是头插法
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

实际插入节点也比较简单,主要是前面进行了检查没有相同的key,这里直接采用头插法插入了,这里的扩容单独拿出来分析吧resize()方法

扩容resize

当超过原来阈值并且本次插入hash冲突的时候触发的扩容

    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);
    }

这里扩容比较简单,那么去看下转移节点怎么进行转移的

transfer 转移数据

    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        // 遍历数组
        for (Entry<K,V> e : table) {
            // 数据有值的时候
            while(null != e) {
                // 遍历链表,先拿到下一个节点的引用
                Entry<K,V> next = e.next;
                // 是不是需要rehash
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                // 计算下标
                int i = indexFor(e.hash, newCapacity);
                // 把节点的下一个指针指向新数组
                e.next = newTable[i];
                // 然后新数组放当前节点(头插法)
                newTable[i] = e;
                // 然后到下一个节点
                e = next;
            }
        }
    }

总结下就是转移数据的时候在遍历数组,然后遍历数组上的链表,然后采用头插法,重新计算下标位置,然后next指针指向新数组的节点,然后把节点放进数组,头插法,这里如果两个线程转移数据,会发生一个已经转移过去的数据,然后另一个线程把他的next指针指向数组上的节点,因为转移数据之后头尾是反过来的,这样就成环了,再次get到这里会死循环

分析下get

其实get基本能在put中体现,因为put的时候需要确保现有里面是不是已经存在key了,如果存在key的话是替换value,然后返回的旧的value,简单看下吧

    public V get(Object key) {
    	// 因为null值特殊对待,直接存放到0位置,这里直接去找
        if (key == null)
            return getForNullKey();
    	// getEntry
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
    	// 先取hash
        int hash = (key == null) ? 0 : hash(key);
    	// 然后找到对应位置,进行遍历,找到就返回,采用的是equals,找不到返回空
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

好了,这样HashMap1.7基本分析完了,那么开头提到的问题基本解决,扩展性问题呢,hashMap对象是不是可以为key,有啥需要注意的,因为是先hash再求的equals,如果不重写对象的hash和equals的话,那么就是对象的地址,这样存进去是可以原对象取出来的,再拿相同属性的对象是不能取出来的,如果重写了hash和equals方法呢,那么可能造成的结果呢,就是拿相同属性的对象可以get到value,但是!!,如果存入之后改变了对象的值,导致hash值发生变化,但是原先存入的不会随着重新hash移动位置,那么拿这个对象就不能获取到值了,虽然他们是同一个对象,equal相同也不行,只能找原来相同的hash和equals进行获取,

HashMap1.7和1.8 的区别

后面再分析下HashMap1.8 的源码,他们之间的区别到底有哪些呢,现在还不清楚具体的区别,先说下之前认知的吧,比如hash算法,扩容机制,头插法 尾插法,红黑树结构,具体的差异等下次分析1.8源码给列一下吧

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值