源码分析---HashMap

源码分析—HashMap

1. HashMap构造器
HashMap总共给我们提供了三个构造器来创建HashMap对象。

(1). 无参构造函数public HashMap()
其: 默认容量:16 默认的负载因子:0.75
无参构造函数源码如下:

static final float DEFAULT_LOAD_FACTOR = 0.75f;//加载因子
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

(2). 有参构造函数public HashMap(int initialCapacity,float loadFactor)
该构造函数,可以指定hashmap的初始化容量和负载因子,但是在hashmap底层不一定会初始化成我们传入的容量,而是会初始化成大于等于传入值的最小的2的幂次方,比如我们传入的是17,那么hashmap会初始化成32(2^5)。
那么hashmap是如何高效计算大于等于一个数的最小2的幂次方数的呢,源码如下:

 static final int MAXIMUM_CAPACITY = 1 << 30;
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);
}

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,那么这个数+1则肯定是一个2的幂次方数。举个例子看一下:
在这里插入图片描述
可以看到,它的计算过程是:首先将我们指定的那个数cap减1(减1的原因是,如果cap正好是一个2的幂次方数,也可以正确计算),然后对cap-1分别无符号右移1位、2位,4位、8位、16位(加起来正好是31位),并且每次移位后都与上一个数做按位或运算,通过这样的运算,会使得最终的结果低位都是1。那么最终对结果加1,就会得到一个2的幂次方数。
(3). 有参构造函数public HashMap(int initialCapacity)
该构造函数和上一个构造函数唯一不同之处就是不能指定负载因子。
源码如下:

   public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);//其实还是默认加载因子:0.75
    }

2. HashMap插入机制
(1). 插入方法源码

  public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
   final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //初始化桶数组table,table被延迟插入新数据时再初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //如果桶中不包含键值对节点引用,说明当前数组下标下不存在任何数据,则将新键值对节点的引用存入桶中即可
        if ((p = tab[i = (n - 1) & hash]) == null)//从底层数组取值
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
             //如果hash相等,并且equals方法返回true,这说明key相同,此时直接替换value即可,并且返回原值
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))//又看到了熟悉的equals方法,这里我们hash值相等,key的值也相等,条件成立,把值赋值给e。(如果key的值不相等,就比较equals方法,也就是说,就算key是一个新new出来的对象,只要满足equals,也视为key相同)
                e = p;//底层数组元素匹配成功,赋值给e
             //如果第一个节点是树节点,则调用putTreeVal方法,将当前值放入红黑树中
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            	//如果第一个节点不是树节点,则说明还是链表节点,则开始遍历链表,将值存储到链表合适的位置
                for (int binCount = 0; ; ++binCount) {
                    //如果遍历到了链接末尾,则创建链表节点,将数据存储到链表结尾
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);//把元素放到最后
                        //判断链表中节点树是否超多了阈值8,如果超过了则将链表转换为红黑树(当然不一定会转换,treeifyBin方法中还有判断)
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st   如果长度超>=8,转换成红黑树
                            treeifyBin(tab, hash);//转换成红黑树
                        break;//如果底层数组元素第一个没匹配上,循环链表,直到匹配成功为止
                    }
                    //如果在链表中找到,完全相同的key,则直接替换value
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;//如果hash值相等,key也相等或者equals相等,赋值给e
                }
            }
             //e!=null说明只是遍历到中间就break了,该种情况就是在链表中找到了完全相等的key,该if块中就是对value的替换操作
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);//用新的value替换旧value并返回旧的value
                return oldValue;
            }
        }
        ++modCount;
        //加入value之后,更新size,如果超过阈值,则进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
  final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                   int h, K k, V v) {
        Class<?> kc = null;
        boolean searched = false;
        TreeNode<K,V> root = (parent != null) ? root() : this;
        for (TreeNode<K,V> p = root;;) {//遍历树的节点
            int dir, ph; K pk;
            if ((ph = p.hash) > h)
                dir = -1;
            else if (ph < h)
                dir = 1;//如果put的key==或equals节点的key,返回该节点
            else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                return p;
            else if ((kc == null &&
                      (kc = comparableClassFor(k)) == null) ||
                     (dir = compareComparables(kc, k, pk)) == 0) {
                if (!searched) {
                    TreeNode<K,V> q, ch;
                    searched = true;
                    if (((ch = p.left) != null &&
                         (q = ch.find(h, k, kc)) != null) ||
                        ((ch = p.right) != null &&
                         (q = ch.find(h, k, kc)) != null))
                        return q;
                }
                dir = tieBreakOrder(k, pk);
            }

            TreeNode<K,V> xp = p;
            if ((p = (dir <= 0) ? p.left : p.right) == null) {
                Node<K,V> xpn = xp.next;
                TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                if (dir <= 0)//遍历完后还是没找到key,在树中添加新节点
                    xp.left = x;
                else
                    xp.right = x;
                xp.next = x;
                x.parent = x.prev = xp;
                if (xpn != null)
                    ((TreeNode<K,V>)xpn).prev = x;
                moveRootToFront(tab, balanceInsertion(root, x));
                return null;
            }
        }
    }

(2) .插入流程图
在这里插入图片描述
① 在put一个k-v时,首先调用hash()方法来计算key的hashcode,而在hashmap中并不是简单的调用key的hashcode求出一个哈希码,还用到了扰动函数来降低哈希冲突。源码如下:

   /**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */
static final int hash(Object key) {
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//高16位异或低16位
 }
//从源码中可以看到,最终的哈希值是将原哈希码和原哈希码右移16位得到的值进行异或运算的结果。16正好是32的一半,因此hashmap是将hashcode的高位移动到了低位,再通过异或运算将高位散播的低位,从而降低哈希冲突。

至于为什么能够降低冲突呢,从作者对hash方法的注释中我们可以得出:
作者进行高位向低位散播的原因是:由于hashmap在计算bucket下标时,计算方法为hash&n-1,n是一个2的幂次方数,因此hash&n-1正好取出了hash的低位,比如n是16,那么hash&n-1取出的是hash的低四位,那么如果多个hash的低四位正好完全相等,这就导致了always collide(冲突),即使hash不同。因此将高位向低位散播,让高位也参与到计算中,从而降低冲突,让数据存储的更加散列。

②. 在计算出hash之后之后,调用putVal方法进行key-value的存储操作。
在putVal方法中首先需要判断table是否被初始化了(因为hashmap是延迟初始化的,并不会在创建对象的时候初始化table),如果table还没有初始化,则通过resize方法进行扩容。

if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

③. 通过**(n-1)&hash计算出当前key所在的bucket下标,如果当前table中当前下标中还没有存储数据,则创建一个链表节点直接将当前k-v存储在该下标的位置**。

if ((p = tab[i = (n - 1) & hash]) == null)
     tab[i] = newNode(hash, key, value, null);

④. 如果table下标处已经存在数据,则首先判断当前key是否和下标处存储的key完全相等,如果相等则直接替换value,并将原有value返回,否则继续遍历链表或者存储到红黑树。

if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
      e = p;

⑤. 当前下标处的节点是树节点,则直接存储到红黑树中

else if (p instanceof TreeNode)
         e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

⑥. 如果不是红黑树,则遍历链表,如果在遍历链表的过程中,找到相等的key,则替换value,如果没有相等的key,就将节点存储到链表尾部(jdk8中采用的是尾插法),并检查当前链表中的节点树是否超过了阈值8,如果超过了8,则通过调用treeifyBin方法将链表转化为红黑树。

for (int binCount = 0; ; ++binCount) {
      if ((e = p.next) == null) {
          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))))
          break;
      p = e;
  }

⑦. 将数据存储完成之后,需要判断当前hashmap的大小是否超过扩容阈值Cap*load_fact(注意此处),如果大于阈值,则调用**resize()**方法进行扩容。

f (++size > threshold)
       resize();

HashMap在扩容后的容量为原容量的2倍,起基本机制是创建一个2倍容量的table,然后将数据转存到新的散列表中,并返回新的散列表。和jdk1.7中不同的是,jdk1.8中多转存进行了优化,可以不再需要重新计算bucket下标,其实现resize()部分源码如下:
在这里插入图片描述
从源码中我们可以看出,如果一个key hash和原容量oldCap按位与运算结果为0,则扩容前的bucket下标和扩容后的bucket下标相等,否则扩容后的bucket下标是原下标加上oldCap。

使用的基本原理总结如下:

1、如果一个数m和一个2的幂次方数n进行按位与运算不等于0,则有:m&(n²-1)=m&(n-1)+n理解:一个2的幂次方数n,在二进制中只有一位为1(假设第k位是1),其他位均为0,那个如果一个数m和n进行按位与运算结果为0的话,则说明m的二进制第k位肯定为0,那么m的前n位和前n-1位所表示的值肯定是相等的。

2、如果一个数m和一个2的幂次方数n进行按位与运算等于0,则有:m&(n²-1)=m&(n-1)理解:一个2的幂次方数n,在二进制中只有一位为1(假设第k位是1),其他位均为0,那个如果一个数m和n进行按位与运算结果不为0的话,则说明m的二进制第k位肯定为1,那么m的前n位和前n-1位所表示的值的差恰好是第k位上的1所表示的数,二这个数正好是n。
在这里插入图片描述
3. 小结
在hashMap中放入(put)元素,有以下重要步骤:
1、计算key的hash值,算出元素在底层数组中的下标位置。
2、通过下标位置定位到底层数组里的元素(也有可能是链表也有可能是树)。
3、取到元素,判断放入元素的key是否或equals当前位置的key,成立则替换value值,返回旧值。
4、如果是树,循环树中的节点,判断放入元素的key是否
或equals节点的key,成立则替换树里的value,并返回旧值,不成立就添加到树里。
5、否则就顺着元素的链表结构循环节点,判断放入元素的key是否==或equals节点的key,成立则替换链表里value,并返回旧值,找不到就添加到链表的最后。

精简一下,判断放入HashMap中的元素要不要替换当前节点的元素,key满足以下两个条件即可替换:
1、hash值相等。
2、==或equals的结果为true。

由于hash算法依赖于对象本身的hashCode方法,所以对于HashMap里的元素来说,hashCode方法与equals方法非常的重要。重写对象的equals方法一定要重写hashCode方法的原因,不重写的话,放到HashMap中可能会得不到你想要的结果!

4. HashMap知识点小结
HashMap:数组+链表+红黑树
负载因子:0.75
默认长度:16
扩容大小:2倍 扩容后元素重新分配 hash(key) 算法中的n-1尽可能都是1,才离散
链表元素存储:
1.7:头节点插入
1.8:尾节点插入
hash计算:
1.7:九次扰动
1.8:两次扰动
key.hashCode()的高16位 ^ 低16位
即:h = (h = key.hashCode()) ^ (h >>> 16)
下标确定:h & (n-1) 减少hash冲突
put过程:
树化:链表长度>=8 防止哈希表碰撞攻击;提高map效率
链化:链表长度<=6

HashTable: 线程安全,不建议使用
ConcurrentHashMap:线程安全,使用较多
1.7:分段锁机制实现
1.8:synchronized实现

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值