一文读懂HashMap1.7/1.8源码

326 阅读4分钟

前言:一文读懂HashMap

本文从初始化、put()两个角度出发,领略HashMap源码,内容包含各参数解释、扩容机制、hash算法及JDK1.7和JDK1.8的不同,在文末会附上面试题及答案。

参数解析

capacity:容量 是一个抽象概念,表示里面HashMap中含有的key-value对个数,用属性size表示

transient int size;

threshold:阀值 是否需要扩容的标记 loadFactory:加载因子,用于计算threshold 加载因子为float类型 threshold = capacity * loacFactory;

1. HashMap初始化

1.1 HashMap1.7初始化

初始化的目的:初始化各参数、初始化内部的table::Entry字段

只需阅读无参构造方法,了解JDK1.7中HashMap的初始化发生在new HashMap中
a. 无参构造方法
HashMap(){   
    // 1.初始化参数
    this.loadFactor = DEFAULT_LOAD_FACTOR; // 默认 DEFAULT_LOAD_FACTOR=0.75f
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    // 2.为table字段赋值
    table = new Entry[DEFAULT_INITIAL_CAPACITY];
}
b. 有参构造方法
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
    // 1.进行参数校验
    ...
    // 2.初始化参数
    this.loadFactor = loadFactor;
    threshold = (int)(capacity * loadFactor);
    // 3.为table赋值
    table = new Entry[capacity];
    // 4.init方法为一个扩展方法,HashMap中并无实际作用
    init();
}

1.2 HashMap1.8初始化

JDK1.8中构造方法中只完成了“部分”参数的赋值操作,且并未真正构造table::Node对象。

    a.无参构造方法
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    b.有参构造方法
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
    public HashMap(int initialCapacity, float loadFactor) {
        1. 检验参数
        2. 部分属性赋值
        this.loadFactor = loadFactory
        // 查找距离initialCapacity最近的2的幂次方:如:initialCapacity = 10,则 
        this.threshold = 16this.threshold = tableSizeFor(initialCapacity);
        
    }
    

可以看出JDK1.8中构造方法并未真正的进行初始化操作,只是对部分属性赋值

2.HashMap中put()方法

不管1.7还是1.8,put()方法的目的都是保证给HashMap增加一个key-value对。这其中涉及到的扩容:resize()、hash和索引计算等知识。

2.1 JDK1.7中的put()操作

put()流程:

  1. 判断table是否为空:若为空则初始化(JDK1.7中不会出现为空的情况)
  2. 判断是否已存在相同key
  3. 不存在相同key:则调用addEntry()增加entry节点 HashMap扩容.png

image.png

public V put(K key, V value) {
    // 1. 判断table是否为空:若为空则初始化
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key);
    int i = indexFor(hash, table.length);
    // 2. 判断是否存在相同key
    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))) {
            // 判断key相同 替换旧值
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    // 3. 无相同key:直接添加数据
    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

让我们深入探究put()方法的内涵,学习完你将了解以下知识

  1. 怎么计算hash和索引,如果hashCode冲突了,那么会发生什么?
  2. HashMap中如何判断key相同?如果新插入元素中索引位置有元素,会发生哪些情况?JDK1.7中HashMap采用的是头插法还是尾插法?请描述一下插入的整个流程?
  3. JDK1.7中扩容发生在哪些时刻?先插入元素再扩容还是先扩容再插入元素?

让我们来看看第一个问题:如何计算hash和索引?

让我们来看看第一个问题:

  • HashMap中如何判断key相同? :首先判断索引处是否有值,有值则遍历相同索引的值,判断是否存在相同的key 首先hash相同其次key的索引或内容相同 if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
  • 如果新插入元素中索引位置有元素,会发生哪些情况? 答:如果索引位置有元素,则分为key相同/key不同,key相同则直接替换旧值即可,key不同则程序往下运行,后续统一利用addEntry()方法插入。
  • JDK1.7中HashMap采用的是头插法还是尾插法? 答:要想搞懂这个问题,需要看看addEntry()源码
    // 3. 无相同key:直接添加数据
    modCount++;
    addEntry(hash, key, value, i);
    return null;
    
    void addEntry(int hash, K key, V value, int bucketIndex) {
        // a.判断是否需要扩容 
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        // b.创建元素
        createEntry(hash, key, value, bucketIndex);
    }
    
    void createEntry(int hash, K key, V value, int bucketIndex) {
           // 取出当前位置的元素,如果是新添加的key,则e为null,已经有的元素为不为空。
            Entry<K,V> e = table[bucketIndex];
            // 添加新的key-value值或构建链表
            table[bucketIndex] = new Entry<>(hash, key, value, e);
            size++;
    }

所以可以看出是采用头插法插入元素,原索引处元素(或为null)作为新元素的next

  • JDK1.7中扩容发生在哪些时刻? 答:扩容采用resize()方法 扩容步骤
  1. 设置新的entry
  2. 转移旧元素至新的entry上(原索引为i的数据在新entry中索引是?)

2.2 JDK1.8中的put()操作

JDK1.8中原数组+链表的数据结构修改为数组+链表/红黑树 阅读完本节,你将了解

  1. JDK1.8中初始化的时间?
  2. JDK1.8中扩容时机
  3. JDK1.8中hash和索引计算

回归put()方法本身,put()目的加入一个元素,整体流程图如下

image.png

由流程图可以得出:

  1. JDK1.8中初始化的时间? 答:真正的初始化发生在put(Object key)时
  2. JDK1.8中扩容时机? 答:先添加节点再判断是否需要扩容
  3. JDK1.8中hash和索引计算
index:i
hash = hashCode^hashCode>>>16;
i = e.hash & (newCap - 1);