详谈HashMap源码以及哈希碰撞

0.底层结构

首先看HashMap的底层结构和基本方法

transient Node<K,V>[] table;  //数组,hash表的桶
//hash表每个桶下的链表
 Node(int hash, K key, V value, Node<K,V> next) {
      this.hash = hash;
      this.key = key;
      this.value = value;
      this.next = next;
        }

还要注意的是,HashMap不是一个线程安全的集合!!!
在这里插入图片描述
都知道Map有一个很标志的特点,存储键值对。

在这里插入图片描述
就是这个接口!Entry!

//An object that maps keys to values.  
interface Entry<K,V>{
	K getKey();
	V getValue();
}
  • Map集合迭代器输出

      //1.Map->Set
      Set<Map.Entry<Integer, String>> key = map.entrySet();
      //2.取得Set迭代器
      Iterator<Map.Entry<Integer, String>> iterator = key.iterator();
      //3.迭代输出
      while (iterator.hasNext()){
          Map.Entry<Integer,String> entry = iterator.next();
          System.out.println(entry.getKey()+"="+entry.getValue());
      }
    

了解一下HashMap的各种参数~,马上开始看源码


static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//初始化容量为16(桶的数量)
static final int MAXIMUM_CAPACITY = 1 << 30;
//最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//负载因子:0.75
static final int TREEIFY_THRESHOLD = 8;
//树化门限值:8
static final int UNTREEIFY_THRESHOLD = 6;
//解树化,返回链表的阈值:6
static final int MIN_TREEIFY_CAPACITY = 64;
//树化的最少元素个数:64


1.创建HashMap

 public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    //DEFAULT_LOAD_FACTOR->负载因子,默认0.75
 }

    transient int modCount; //提一嘴这个属性,记录HashMap更改次数

很明显HashMap的创建并没有大张旗鼓地开始初始化Hash表,仅仅只是初始化了负载因子?(无参构造)
那什么时候开始真正地创建呢?


2.添加元素

public V put(K key,V value) {
    return putVal(hash(key), key, value, false, true);
}

再看看putVal方法~

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    if ((tab = table) == null || (n = tab.length) == 0)
        //调用resize方法初始化
        n = (tab = resize()).length;
        
    //判断hash桶中是不是装有结点
    if ((p = tab[i = (n - 1) & hash]) == null)
        //没有结点,将key和value值封装成一个node结点放入对应的桶中
        tab[i] = newNode(hash, key, value, null);
    else {
    
        Node<K,V> e; K k;
        //当put的key值是已有值时
        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);
     
       //若未树化
        else {
            for (int binCount = 0; ; ++binCount) {
                //在链表中尾插进该结点,并跳出循环
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);
                    break;
                }
                //若该key值已经存在
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        
        //若key值重复,更新value值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    
    ++modCount;
    //判断是否需进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

嗯,原来真正的初始化在这!resize(),最后好像扩容也用到了?
是的,resize()负责hash表的初始化和扩容


3.扩容

主要代码如下:扩容2倍


    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        //读取旧桶大小、旧的扩容阈值。
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
       
//-----------------------扩容功能---------------------
        //旧桶不为空
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
            //旧桶大小已经大于1<<30大小了!
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //容量*2
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
             //旧桶还可以扩容,阈值扩大2倍!
                newThr = oldThr << 1; // double threshold
        }
        
//-----------------------初始化功能---------------------

		//旧桶为空,但是旧的阈值大于0(有参构造)
        else if (oldThr > 0) // initial capacity was placed in threshold
        	//新容量就等于旧的阈值
            newCap = oldThr;
            
        else {               // zero initial threshold signifies using defaults
        	//否则新容量、阈值为默认大小
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        
        //新阈值是0(有参构造传入的容量*0.75太小,为0)
        //重新设置
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        
        //最后更新阈值
        threshold = newThr;
        }
   }


4.树化

主要看这段~,最开始说了树化阈值是8,又说了树化的最大元素是64.
只有同时满足才会树化一个桶!

    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        
        //hash表为空||表内全部元素小于阈值(默认64)
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
       		//扩容
            resize();
            


5.一个元素怎么存到Hash表中?

  1. hashCode()这是一个native本地方法,不需要了解

  2. hash(Object)
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

  3. putVal()中
    p = tab[i = (n - 1) & hash]

  • 也就是先通过native方法计算这个对象的hash值,每个对象有一个hash值。我们可以看到hashCode()的返回值是int类型,int大概是2^31左右。肯定不能直接放进数组!
  • hash()进行处理,要是为key为null,结果为0,否则把原来的hash值和hash右移16位后的值做^运算,返回这个值。
  • 在putVal中,将上一步处理好的hash值和hash表的长度-1做&运算,得到最后的下标。

至于为什么,下面讲解!


6.许多问题的解答~

问题1:为什么树化?

因为可以大幅度缩减查找元素的时间,红黑树的查找比链表的查找快了O(n)/O(log2 n)倍。

问题2:怎么树化?

如果容量小于树化阈值64,就只会简单的扩容,若大于并且桶中元素大于8个,则树化。

HashMap和HashSet初始化方式是一样的,都是懒加载策略,也就是一开始构造里面不做真正的初始化,在第一次添加元素的时候才真正初始化。

resize()方法有两个作用:1.初始化数组(桶)2.扩容

问题3:扩容多少?

初始化一般是默认容量16.

扩容:newCap = oldCap<<1;每次都是*2

问题4:负载因子和容量?

负载因子就相当于可用余额占全部空间的百分比,一般是0.75

容量就是真正的总空间大小,有一点防患于未然的意思。

阈值(可用余额) = 负载因子*容量

初始阈值 = 负载因子(默认0.75)*默认容量(也就是初始化容量,16) = 12

newThr = oldThr <<1;每次扩容2倍

这个阈值就是桶的最大可用空间大小,一旦超过,就会扩容,但其实还有0.25的空间,就是提前预防。

问题5:那hashcode怎么和桶的i联系起来呢?

上文已经说明,hashMap通过两次处理得到i值,起作用很明显,就是避免哈希碰撞!
第一次把原始的hash值和右移16位的自己异或运算,作用打乱了hash的顺序,避免碰撞。
第二次再和n-1做与运算,这一步作用就是规范hash大小(规范到n以内)。

问题6:为什么hash表的length最好是2^n?

2^n-1就相当于后面的低位都变成了1,那么&操作的结果最大程度上取决于hash地址,唯一性提高,而且位运算效率更高。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值