HashMap1.8源码解读

1,137 阅读9分钟
  1. HashMap的初始值
  2. 思考:为什么负载因子是0.75
  3. HashMap的存储结构
  4. 时间复杂度效率对比
  5. new 一个HashMap的一些方法 
  6. put方法 
  7. 思考:为什么链表长度达到8之后转变成红黑树结构?
  8. resize方法 
  9. get方法 
  10. getNode方法



HashMap的初始值

//初始化容量(必须是二的n次幂)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//集合的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;

//负载因子,默认0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//当链表的值超过8,链表会转换成红黑树
static final int TREEIFY_THRESHOLD = 8;

//当链表的值小于6会从红黑树转换为链表
static final int UNTREEIFY_THRESHOLD = 6;

/*
 * 哈希表中的容量超过这个值时,才将链表转换成红黑树。否则桶内元素太多,直接扩容,而不是树形化。
 * 为了避免扩容、转换红黑树之间的冲突,该值不能小于4 * TREEIFY_THRESHOLD
 */
static final int MIN_TREEIFY_CAPACITY = 64;//初始化table的大小,必须是2的n次幂
transient Node<K,V>[] table;


//存放缓存大小
transient Set<Map.Entry<K,V>> entrySet;


//HashMap的存储大小
transient int size;


//用来记录HashMap的修改次数
transient int modCount;

/**
 * The next size value at which to resize (capacity * load factor).
 *
 * @serial
 */
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)

//(负载因子*容量):扩容数组的时候用来计算新容器的大小计算方式
int threshold;

/**
 * The load factor for the hash table.
 *
 * @serial
 */
//哈希表的加载因子
final float loadFactor;


//创建一个指定大小的HashMap,负载因子0.75
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 */
//创建一个空的HashMap 默认大小16,负载因子0.75
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/**
 * Returns a power of two size for the given target capacity.
 *
 * 返回 cap 初始值的2的幂
 * 作用:将传入的容量大小转化为:>传入容量大小的最小的2的幂
 */
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;
}


思考:为什么负载因子是0.75

负载因子的作用是和扩容机制相关联的,HashMap初始值数组长度为16,当数组长度达到 16*0.75=12的时候就会对数组进行扩容。这个12就是通过负载因子计算出来的阈值。

那么为什么默认是0.75呢?为什么不是1,不是0.5。HashMap1.8的结构是数组+链表+红黑树组成的。在HashMap 发生哈希碰撞之后,会在同一个节点中挂一个链表添加相同的哈希值,当链表长度达到8的时候会转变成红黑树,时间复杂度从链表的O(n)变为O(logn)。如果负载因子是1,那么也就是当数组长度达到16的时候才能进行扩容,这就意味着大量的哈希冲突会出现,在遍历红黑树获取值的时候时间会更久,相当于提升了空间利用率,牺牲了时间。

相对的,如果负载因子默认是 0.5 ,数组长度达到一半就会扩容,那么空间利用率就会降低。

如果你想问为什么是0.75最合适而不是0.8呢?(大佬说是0.75就是0.75嘛,你有什么好杠的~)


HashMap的存储结构



HashCode方法发生哈希碰撞的情况下,在相同元素创建一个链表,把所有相同的元素存放在链表中

img

可以看出T1的哈希和T2相同,但是元素不同,所以现在会形成一个链来存储。

但是如果链表过长,HashMap会把这个链表转换成红黑树


时间复杂度效率对比

时间复杂度的优劣对比常见的数量级大小:越小表示算法的执行时间频度越短,则越优;

 O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n)


源码的负载因子上也做了相应的解释,但是用翻译软件翻译出来的话总是有点鸡同鸭讲。大概意思就是一句话,0.75是在时间和空间上权衡之后得出的结果。

 /* <p>As a general rule, the default load factor (.75) offers a good
 * tradeoff between time and space costs.  Higher values decrease the
 * space overhead but increase the lookup cost (reflected in most of
 * the operations of the <tt>HashMap</tt> class, including
 * <tt>get</tt> and <tt>put</tt>).  The expected number of entries in
 * the map and its load factor should be taken into account when
 * setting its initial capacity, so as to minimize the number of
 * rehash operations.  If the initial capacity is greater than the
 * maximum number of entries divided by the load factor, no rehash
 * operations will ever occur.*/


new 一个HashMap的一些方法

//创建一个空的HashMap,指定初始化大小和负载因子
public HashMap(int initialCapacity, float loadFactor) {
    //判断初始化大小是否小于0,小于0抛出IllegalArgumentException异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //如果初始容量大于集合的最大容量,将初始容量改成最大容量
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //当负载因子小于0或者是一个非数值,抛出异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

//创建一个指定大小的HashMap,负载因子0.75
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//创建一个空的HashMap 默认大小16,负载因子0.75
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
    //设置默认负载因子
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        if (table == null) { // pre-size
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        else if (s > threshold)
            resize();
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            putVal(hash(key), key, value, false, evict);
        }
    }
}


put方法

  • 1.8的put方法和1.7比较做了比较大的改动过,由1.7的 数组+链表 变成1.8的 数组+链表+红黑树。Node类变成了Entry类
  • 使用resize() 进行扩容时,数据存储的位置会进行重新计算,原有的红黑树长度 < 6 时,会转变成链表

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是否等于空或者等于0 是:使用resize()进行初始化
    * */
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    /*
    * 对hash值进行定位,如果tab[i]等于null ,为null创建一个新的节点,这里是会造成线程安全问题的。
    * */
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e;
        K k;
        //这里进入tab[i]不为空标识这个位置已经存在值
        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 {
            /**
             * 不是红黑树,那就是链表,遍历到节点最后插入,也就是尾插法
             * 这里和 JDK1.7 不同,1.7使用的是头插法,永远添加到数组的头部位置,原来位置上的数向后移动
            */
            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;
            }
        }
        //发现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;

    //sizi是当前集合大小,判断当前集合大小是否超过阈值,超过则调用resize()
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

思考:为什么链表长度达到8之后转变成红黑树结构?

个人认为还是和时间、空间成本有关。红黑树插入、查询的时间复杂度为O(logn),链表为插入的时间复杂度为O(1),查询O(n),根据 O(1) < O(logn) < O(n) 查询数量多时,链表 > 红黑树,为了避免结构频繁的转换,默认负载因子0.75这个值,计算出来的单个Hash槽元素个数达到8的概率极低,所以将7作为分水岭。

源码中的这一部分为

                    //binCount >= 8-1
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;


resize方法

1.8的hashMap在数组大小大于阈值,就进行resize() 进行扩容,扩容为原来的两倍

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) {
        //获取原表的大小和最大值1<<30 进行比较,大于最大值,则将原表大小改成最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //没有达到最大值,进行两倍扩容
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        //初始化集合默认大小16
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //计算阈值的上限
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;

    //创建新数组
    @SuppressWarnings({"rawtypes","unchecked"})
        //新建hash数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //新hash 赋值旧 hash 
    table = newTab;
    //开始扩容
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) { //如果旧的hash数组在j结点处不为空,复制给e
                oldTab[j] = null; //设置为null 方便GC回收
                if (e.next == null)//如果e后面没有结点
                    newTab[e.hash & (newCap - 1)] = e;//获得新的存储位置
                else if (e instanceof TreeNode)  //如果是红黑树类型,添加到红黑树中
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}


get方法

get方法调用的是getNode()

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

getNode方法

final Node<K,V> getNode(int hash, Object key) {
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab;
    Node<K,V> first, e;
    int n;
    K k;
    //判断table长度大于0,根据hash查找tab中的元素不为null
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        //查找Node中的下一个元素
        if ((e = first.next) != null) {
            //如果是红黑树结构
            if (first instanceof TreeNode)
                //在红黑树中查找该元素
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                //在链表中查找该元素
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}