HashMap源码解析(一)

2021-12-04 11:24:28 浏览数 (1)

HashMap是面试中经常被问到的一个问题,不管是初级还是中级以及高级,都会问到,只不过深度不一样,今天我们就详细解析一下HashMap的源码,为了大家能碎片时间看完,我们分为多篇文章解析,首先从1.7JDK版本开始解析,

基本概念我且以为大家都知道,那么我们开始1.7JDK的源码看起,下图就是HashMap数据结构

put方法

代码语言:javascript复制
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)   //1.key为null处理
            return putForNullKey(value);
        int hash = hash(key); //2.计算hash值
        int i = indexFor(hash, table.length);//3.找到数组的下表
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {  //4.遍历链表找到对应的值
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                //5.如果找到对应的key,就会覆盖值,返回老值oldValue
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
          //6.如果找不到就新增一个Entry节点
        modCount  ;
        addEntry(hash, key, value, i);
        return null;
    }
  1. 首先处理key=null的情况
  2. 如果不等于null,就会计算key的hash值
  3. 找到数组的下标
  4. 根据下标循环遍历链表
  5. 如果找到对应的key,就会覆盖值
  6. 如果没有找到就会新增一个Entry节点

然后我们再看看put里面的重要逻辑方法,首先我们看看

putForNullKey

代码语言:javascript复制
    private V putForNullKey(V value) {
       //1.对于key=null的值,统一放到table[0]中
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
          //2.找到key为null的值,就会替换旧值,然后旧值
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount  ;
        //3.这里是在table[0]中没有找到,则新建一个key=null的entry
        addEntry(0, null, value, 0);
        return null;
    }


    void addEntry(int hash, K key, V value, int bucketIndex) {
        //4.如果size超过threshold且table[0]不为null,
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //5.进行扩容
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
             //6.重新进行计算下标
            bucketIndex = indexFor(hash, table.length);
        }
        //7.使用头插法进行新增节点
        createEntry(hash, key, value, bucketIndex);
    }

    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size  ;
    }

putForNullKey逻辑比较简单,首先选择table[0]位置的链表,然后对链表做遍历找到key=null的节点,将新值替换旧值,然后旧的值,如果未找到,则新增一个key为null的Entry节点

其中addEntry的逻辑是新增一个entry,他还负责扩容机制,扩容之后就会进行迁移数据,即把旧数组的链表转移到新数组,然后重新计算下标,然后使用头插法新增节点

hash方法和indexFor方法

代码语言:javascript复制
   //扰动函数就是为了减少hash冲突
    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h ^= k.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
    //这里使用与运算,性能会更好,且hashMap的长度是2的次幂
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

这里hash函数是为了尽可能的均匀分布,减少hash冲突,其中就是想保留高位和低位,然后高位和低位进行与运算,而indexFor是为了计算数组下标即实际的存储位置,使用与位运算是为了更高的性能

这里为什么HashMap的长度必须是2的次幂

  • 降低发生碰撞的概率,使散列更均匀,
  • 如果是长度是2的次幂,那么length-1的二进制最后一位一定是1,在hash值做与运算的时候,最后一位就可能是1,也可能是0,即不是偶数就是奇数,但是如果length不是2的次幂,则length-1就是一个偶数,因此导致h&(length-1)只能是奇数,就会浪费一半的空间

get方法

代码语言:javascript复制
      public V get(Object key) {
        if (key == null) 
           //1.如果key=null,则遍历数组的table[0],获取到对应的value,否则返回null
            return getForNullKey(); 
          //2.key!=null,调用下面方法获取
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }


    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

    final Entry<K,V> getEntry(Object key) {
       //3.如果size=0.则返回null
        if (size == 0) {
            return null;
        }
       //4.计算key的hash值
        int hash = (key == null) ? 0 : hash(key);
      //5.根据hash值计算下标,然后循环遍历链表获取对应可以的value
        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;
    }

get方法就比较简单,如果key=null,就在table[0]链表中获取对应的值,如果key!=null,就计算key的hash值,计算下标位置,然后遍历对应的链表找到对应的value,否则返回null

0 人点赞