java开发:集合(四):hashMap源码解析

HashMap底层存储结构

HashMap是一个用于存储Key-Value键值对的集合,每一个键值对其实就是HashMap内部的Entry类对象。

 static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
        /**
         * Creates new entry.
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

Entry是HashMap的内部类,用来保存我们的键值,next指向下一个节点,hash用来保存key值的哈希码

HashMap它底层是基于数组链表实现的数据存储结构。
HashMap在初始化时会创建一个默认长度为16的数组,当我们添加元素时它会根据key值的哈希码和数组长度取余得到元素在数组的存储位置。但是存在的问题就是不同的key值在经过计算之后可能会映射到相同的位置上,当插入一个元素时,发现该位置已经被占用,这时候就会产生冲突,也就是所谓的哈希冲突,所以HashMap结合链表正是解决了位置冲突问题。
HashMap 设置数组的每一个元素对应一个链表的头结点。当位置发生冲突时就往该链表的头部插入新的节点,新的节点指向旧的头结点。

在这里插入图片描述
HashMap存储数据的流程:

如上图: 当添加一个新的元素时先计算出元素在数组的存储下标,如果位置是空的直接插入到数组,如果位置不为空判断key值是否相等,相等则覆盖value值,不相等则历遍链表。历遍链表结束后key还是没找到则往链表的头部插入新的节点。
像上图数组第一个位置存放着一个Entry对象,当插入新Entry对象计算出的位置也是数组的第一个位置,这时候发生哈希冲突了。系统会把新的Entry插入到数组的第一个位置,并且新的Entry.next属性指向旧的Entry对象。

HashMap数据查找流程:

因为HashMap在内部维护这一个数组table,数组的每个位置保存着每个链表的表头结点,查找元素时,先通过hash函数得到key值对应的hash值,再根据hash值和数组长度计算得到在数组中的索引位置,拿到对应的链表的表头,最后去遍历这个链表,得到对应的value值。

put()方法源码解析:

 public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        //计算key的哈希值
        int hash = hash(key);
        //根据key哈希值和数组长度计算出存储下标
        int i = indexFor(hash, table.length);
        //历遍table[i]整个链表,如果出现key重复的则覆盖value值,然后return结束程序
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            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++;
        //上面的for循环结束后没有发现key没有重复则会执行这个方法
        addEntry(hash, key, value, i);
        return null;
    }


 private V putForNullKey(V value) {
 		//获取数组的第一个位置元素,历遍链表找到key为null的键值对然后覆盖value值
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                //结束方法
                return oldValue;
            }
        }
        modCount++;
        //上面的for循环结束后没有发现key为null的元素则会执行这个方法
        addEntry(0, null, value, 0);
        return null;
    }

	
  void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            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) {
 		//根据bucketIndex获取数组指定位置的元素,e 是链表的头结点
        Entry<K,V> e = table[bucketIndex];
        //创建节点放到数组中,这时候该节点成为头结点,同时它的next指向上一个头结点e
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

先是判断key是否为null,是则执行putForNullKey(),putForNullKey历遍table[0]整个链表,如果有key等于null的元素时则覆盖value值,然后结束程序(因此haspMap只能有一个key为null的元素)。如果table[0]整个链表没有key等于null的元素则执行 addEntry(0, null, value, 0),addEntry调用createEntry(),createEntry就是将table[0]的元素取出来,然后把新的元素放到table[0]中,同时新的元素指向旧的元素,链表size++
当key不为null时,先根据key的哈希值和数组长度计算出存储的下标位置,历遍table[i]的整个链表看有没有key重复,如果出现key重复的则覆盖value值,然后return结束程序。否则执行addEntry(),addEntry调用createEntry(),createEntry就是将table[i]的元素取出来,然后把新的元素放到table[i]中,同时新的元素指向旧的元素,链表size++

get()方法源码解析:

 public V get(Object key) {
 		//先判断key是否为null,是则执行getForNullKey
        if (key == null)
            return getForNullKey();
            //key不等于null,执行getEntry
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
    }
    
    
  private V getForNullKey() {
  		//因为key=null的元素hashMap都是存放在table[0]指向的链表中
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        	//历遍找到key=null的元素
            if (e.key == null)
                return e.value;
        }
        return null;
    }
    
 final Entry<K,V> getEntry(Object key) {
 		//计算key的哈希值
        int hash = (key == null) ? 0 : hash(key);
        //indexFor(hash, table.length)是根据key哈希值和table长度计算元素在数组的索引,然后for循环历遍整个table[i]
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            //比较hash值和key值,找到元素后返回
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

先是判断key是否为null,是则调用getForNullKey(),getForNullKey内部则会历遍table[0]指向的链表。
若key不等于null,则调用getEntry(),getEntry根据计算算出数组下标i,然后历遍table[i],找不到元素返回null。

最后声明一点,这是基于jdk1.7的源码分析。1.8后对hashMap进行了优化。
1.7采用数组+单链表,1.8在单链表超过一定长度后改成红黑树存储
1.7扩容时需要重新计算哈希值和索引位置,1.8并不重新计算哈希值,巧妙地采用和扩容后容量进行&操作来计算新的索引位置。
1.7插入元素到单链表中采用头插入法,1.8采用的是尾插入法。

因此引申俩个问题:
1.为啥1.7之前元素添加是采用头插入法

因为hashMap设计者们认为新加的数据被访问的几率大于旧的数据,所以放在前面访问更快。

2.为啥1.8之后元素添加改为了尾插入法

HashMap在jdk1.7之前采用头插入法,在扩容时会导致链表的顺序倒置,在线程并发的情况下扩容容易导致链表死循环(即俩个节点的next节点相互指向对方),并且新加的数据被访问的几率大于旧的数据这个说法并不成立,而尾插法在扩容的时候节点顺序不会打乱。
jdk1.8之后HashMap为何从头插入改为尾插入

3.1.8之后对计算元素索引进行了优化。未扩容前HashMap通过哈希值的二进制和数组长度-1的二进制进行按位与运算得到的结果就是下数组的索引,(图是网上复制的)

&是二进制“与”运算,参加运算的两个数的二进制按位进行运算,运算的规律是:
0 & 0=0
0 & 1=0
1 & 0=0
1 & 1=1

例如:一个key的哈希值二进制是 0001 1010 ,数组长度是n=16,二进制:10000,n-1二进制是1111
哈希值和n-1进行与运算得到二进制:1010 转成十进制就是10,即索引就是在table[10]

在这里插入图片描述
当数组扩容后n=32 ,二进制是:100000,n-1的二进制是:11111(n-1的最高位和旧数组的最高位相同),当然我们依旧通过上诉的计算也是可以得到每个元素的索引,但是没必要。你会发现当n-1的最高位对应的哈希值二进制数是0的话计算出来的索引不变,对应的是1则计算出来的结果是原位置+旧数组长度。
例如下图:扩容后n-1的最高位是1(往左数第五个数),最高位对应hash1的二进制数是1,因此计算出来的结果是26,最高位对应hash2的二进制数是0,因此索引保持不变。又因为旧数组的最高位和n-1的最高位是一样的,因此扩容的时候HashMap通过(e.hash & oldCap) == 0判断节点是否为新位置节点,等于1则移动到原位置+旧数组长度的索引(数组长度永远是2的次幂,二进制只有最高位是1其他是0,因此不管谁和它进行&算要么得1要么得0)
在这里插入图片描述

现在推荐使用 ConcurrentHashMap,它是Java中的一个线程安全且高效的HashMap实现。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值