jdk1.8集合源码分析系列-3-HashMap

274 阅读12分钟

在了解hashMap之前,我们先来简单的回顾一下hash表这种数据结构。

数据结构-hash表

构建hash表的过程中,hash冲突在所难免,但是hash冲突发生的可能性有大有小,主要跟以下三个因素有关:

  • 装填因子:装填因子=元素个数/哈希表空间长度m。很显然,装填因子越大,冲突越容易发生。
  • 哈希函数:当发生hash冲突的时候,使用哪种hash函数实现进行元素存储的路由。最常用到的就是“除模法”。
  • 处理hash冲突的思路:发生hash冲突的时候,用哪种方式来解决冲突。最常使用的就是链表冲突法。

之所以要介绍这些概念就是因为接下来介绍hashMap的时候,我们需要了解它是怎么解决这些问题的。还是老规矩,看hashmap源码之前先看它的接口继承图。

接口继承图以及相关方法分析

从接口继承图我们可以看到我们主要需要关注两个接口,一个Map,一个AbstractMap。

Map

父类接口和lambda方法会被省略掉。

public interface Map<K,V> {
    //-----查询操作-------
    //返回kv映射的数量
    int size();
    //如果这个map没有kv映射返回true反之false
    boolean isEmpty();
    //如果此映射包含指定键的映射关系,则返回true。
    boolean containsKey(Object key);
    //如果此映射将一个或多个键映射到指定值,则返回true。
    boolean containsValue(Object value);
    //返回指定键所映射的值;如果此映射不包含该键的映射关系,则返回 null。
    V get(Object key);

    //------修改操作------
    //向map里离添加一个key-value的映射,如果map里面已经有key的映射关系,则会
    //覆盖之前的映射关系
    V put(K key, V value);
    //在map里移除键为key的映射关系。
    V remove(Object key);

    //-----块操作-----
    /**
     * 将m中所有的映射关系添加到map里面,相同的键映射会被覆盖。
     * 注意,这个方法很容发生空指针,m记得要判断空。
     */
    void putAll(Map<? extends K, ? extends V> m);
    //从此map中移除所有映射关系
    void clear();

    //-----视图-----
    // 返回此映射中包含的键的 Set 视图。
    Set<K> keySet();
    // 返回此映射中包含的值的 Collection 视图。
    Collection<V> values();
    // 返回此映射中包含的映射关系的 Set 视图。
    Set<Map.Entry<K, V>> entrySet();
    //映射项,表示map中每一项映射关系。
    interface Entry<K,V> {
        /**
         * 返回此映射项对应的键。
         */
        K getKey();
        /**
         * 返回此映射项对应的值。
         */
        V getValue();
        /**
         * 覆盖此映射项对应的值。
         */
        V setValue(V value);
        // 比较指定对象与此映射项的相等性。
        boolean equals(Object o);
        //返回此映射项的哈希码值。
        int hashCode();
    }

    //-----比较和哈希-----
    //比较指定的对象与此映射是否相等。
    boolean equals(Object o);
    //返回此map的哈希值。
    int hashCode();
}

AbstractMap

AbstractMap 则实现了Map底下一些通用的方法。这些方法大多数依赖这个方法:public abstract Set<Entry<K,V>> entrySet();子类需要实现这个方法。此外AbstractMap还提供了两个entry的数据结构,一个是SimpleEntry,一个ImmutableEntry,下面是部分源码的实现:

public abstract class AbstractMap<K,V> implements Map<K,V> {

    // 查询操作 
    public int size() {
        return entrySet().size();
    }
    public boolean isEmpty() {
        return size() == 0;
    }
    public boolean containsValue(Object value) {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        if (value==null) {
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                if (e.getValue()==null)
                    return true;
            }
        } else {
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                if (value.equals(e.getValue()))
                    return true;
            }
        }
        return false;
    }
    public boolean containsKey(Object key) {
        Iterator<Map.Entry<K,V>> i = entrySet().iterator();
        if (key==null) {
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                if (e.getKey()==null)
                    return true;
            }
        } else {
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                if (key.equals(e.getKey()))
                    return true;
            }
        }
        return false;
    }
    public V get(Object key) {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        if (key==null) {
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                if (e.getKey()==null)
                    return e.getValue();
            }
        } else {
            while (i.hasNext()) {
                Entry<K,V> e = i.next();
                if (key.equals(e.getKey()))
                    return e.getValue();
            }
        }
        return null;
    }

    // 修改操作
    public V put(K key, V value) {
        throw new UnsupportedOperationException();
    }
    public V remove(Object key) {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        Entry<K,V> correctEntry = null;
        if (key==null) {
            while (correctEntry==null && i.hasNext()) {
                Entry<K,V> e = i.next();
                if (e.getKey()==null)
                    correctEntry = e;
            }
        } else {
            while (correctEntry==null && i.hasNext()) {
                Entry<K,V> e = i.next();
                if (key.equals(e.getKey()))
                    correctEntry = e;
            }
        }

        V oldValue = null;
        if (correctEntry !=null) {
            oldValue = correctEntry.getValue();
            i.remove();
        }
        return oldValue;
    }


    // 块操作
    // 注意:之前也讲到addAll putAll xxAll这种方法,使用之前通常都是要对m进行空指针判断的,不然很容易出现问题
    public void putAll(Map<? extends K, ? extends V> m) {
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            put(e.getKey(), e.getValue());
    }
    public void clear() {
        entrySet().clear();
    }

    public abstract Set<Entry<K,V>> entrySet();


    /**
     * 提供了entry的一种简单实现。
     * 可以使用 Map.entrySet().toArray获取到这个实例
     */
    public static class SimpleEntry<K,V>
        implements Entry<K,V>, java.io.Serializable
    {
        private static final long serialVersionUID = -8499721149061103585L;

        private final K key;
        private V value;
    }

    /**
     * 提供了第二个entry的数据结构:不可变entry。
     * 注意:setValue调用就会发生异常,这个机制也就保证了在构造之后不可能发写操作
     * 这就是“不可变”。很多ImmutableXXX基本都是这么实现的。
     */
    public static class SimpleImmutableEntry<K,V>
        implements Entry<K,V>, java.io.Serializable
    {
        private static final long serialVersionUID = 7138329143949025153L;

        private final K key;
        private final V value;

        // setValue的实现直接抛异常
        public V setValue(V value) {
            throw new UnsupportedOperationException();
        }
    }
}

HashMap源码分析

HashMap数据结构和常量分析

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    
    //------常量-------
    //初始化容量16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //最大容量 2的30次方
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //当构造方法没有指定装填因子,就会使用这个默认的装填因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    static final int TREEIFY_THRESHOLD = 8;
    static final int UNTREEIFY_THRESHOLD = 6;
    static final int MIN_TREEIFY_CAPACITY = 64;

    //------数据结构描述------
    //hash桶
    transient Node<K,V>[] table;
    static class Node<K,V> implements Map.Entry<K,V> {
        //hash值,用于判断索引位置
        final int hash;
        final K key;
        V value;
        //下一个节点的地址
        Node<K,V> next;
    }
    
    transient Set<Map.Entry<K,V>> entrySet;
    //代表键值映射个数
    transient int size;
    //modCount已经出现多次,作用都是一样的,fast-fail,详细和看之前的分析
    transient int modCount;
    //计算当前的容量上限,这个值始终是2的n次方
    int threshold;
    //装填因子,文章开头我们讲了这个概念
    final float loadFactor;
}

初始化策略

 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;
    //计算当前的容量上限,这个值始终是2的n次方
    this.threshold = tableSizeFor(initialCapacity);
}

//返回最接近的cap的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;
}

tableSizeFor功能我们这里做一个小测试:

@Test
public void test() {
    System.out.println(tableSizeFor(0)); //结果1,2的0次方
    System.out.println(tableSizeFor(5)); //结果8,2的3次方
    System.out.println(tableSizeFor(7)); //结果8,2的3次方
    System.out.println(tableSizeFor(13));//结果16,2的4次方
    System.out.println(tableSizeFor(17));//结果32,2的5次方
}

//原理如下:假设cap=7
static int tableSizeFor(int cap) {
    int n = cap - 1; // n=6, 二进制表示:00000110
    //  >>>:无符号右移。无论是正数还是负数,高位通通补0。
    n |= n >>> 1;  // 00000110 | 00000011 = 00000111  7
    n |= n >>> 2;  // 00000111 | 00000001 = 00000111  7
    n |= n >>> 4;  // 00000111 | 00000000 = 00000111  7
    n |= n >>> 8;  // 00000111 | 00000000 = 00000111  7
    n |= n >>> 16; // 00000111 | 00000000 = 00000111  7
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; // 00001000 = 8
}

hash函数实现

这个是hashmap的hash函数实现,简述一下流程:

  • 第一步根据object计算出hash值。
  • 第二步再根据hash值计算索引值,算法是:hash & (length - 1)
  • 注意:当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
//第一步:计算hash值
static final int hash(Object key) {
     int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
     //为什么要这么计算  https://stackoverflow.com/questions/45125497/why-return-h-key-hashcode-h-16-other-than-key-hashcode
}

//第二步:计算索引值,注意:1.8没有这个方法,但是所有计算index的地方也是这么实现的,这里就不知道为什么要去掉这个方法。
static int indexFor(int h, int length) {  
    //等价于 取模运算 h % length
    return h & (length-1);  
}

这里可以看到,虽然1.8没有这个indexFor方法,但是所有算索引的地方仍然是这么计算的。

上述有两处代码必须弄明白:

  • 第一个index的计算方法 h & (length-1)为什么等价于h % length。首先我们知道h % length的结果是[0, length - 1],而h & (length - 1) 得到的结果也是这个范围,为什么这么说呢?注意这里有一个小前提,length是2的n次方,length-1用二进制表示0..00011...11的形式,那么h&(length-1)运算很显然,最大得到就是length-1,最小就是0。
  • 第二个,为什么计算hash使用(h = key.hashCode()) ^ (h >>> 16)而不直接使用key.hashCode()?这里给了我们答案。

This may lead to many collisions in some common use cases. For example, assume table.length is a small power of two (like 32 or 64). In this case only the low order bits of the hashcode determine the index. This will cause lots of collisions if your object's hashcode only differ in the upper bits. The bit shift allows the upper bits of the hashcode to also influence the computed index.

简单来说,由于计算index的方法是这个方法h & (length-1),计算hash时主要是靠hash的低位起作用,这样的话,不同对象产生的hashcode这么计算很容易出现hash冲突,原因是它们的低位很有可能一样。于是使用(h = key.hashCode()) ^ (h >>> 16)这种算法是让低位尽可能不一样,原理是拿hashcode的高位和低位做异或操作,异或操作产生0和产生1的概率是一样,是比&和|操作要更好的。

put逻辑分析

添加元素逻辑分析,这里简述一下流程:

  • 如果hash桶都不存在,就创建一个新的hash桶。
  • 如果索引对应的链表不存在就创建一个链表。
  • 在默认情况下,节点会被添加到链表的最后一个位置。
  • 但是如果链表长度 >= 8的话,链表会转换成红黑树。
//添加一个映射关系,如果存在相同key映射则覆盖
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

//如果map获取到key对应的value为空,那么就增加新的kv映射。
public V putIfAbsent(K key, V value) {
    return putVal(hash(key), key, value, true, 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;
    // 1-如果hash桶为空,那么则新创建一个。
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //2-如果哈希表对应的索引为空,则创建出一个新的链表节点出来。
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //如果hash表的链是红黑树实现的
        else if (p instanceof TreeNode)
            //走红黑树添加节点的逻辑
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //如果hash表对应的链是链表实现的
        else {
            //遍历这个链表,找到尾部,把新的节点插入进来
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    //将链表最后一个节点的next指针和新的节点接上。
                    p.next = newNode(hash, key, value, null);
                    //如果发现链表的长度 >= 8 且 节点个数大于64(这个逻辑在treeifyBin内部,如果小于64,直接进行resize操作),那么直接转换成红黑树
                    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;
                //next指针后移
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                // 设置value
                e.value = value;
            afterNodeAccess(e);
            // 返回老的value
            return oldValue;
        }
    }
    ++modCount;
    //如果size超过当前容量上限,注意,这里的容量是装载因子*capacity算出来的
    if (++size > threshold)
        //进行扩容
        resize();
    afterNodeInsertion(evict);
    return null;
}

扩容逻辑分析

扩容流程是这样的:

  • 如果符合扩容条件,则扩容量被,得到一个新的hash桶。
  • 把老的哈希桶的数据逐步迁移到新的hash桶。
  • 返回新的hash桶。
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    
    //如果hash桶索引长度大于0,走下面的扩容逻辑
    if (oldCap > 0) {
        // 超过最大值就不再进行扩容,只能碰撞了。
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 否则就扩容两倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 新的hash桶的容量上限
            newThr = oldThr << 1; // double threshold
    }
    //初始化指定索引长度逻辑走这里
    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);
    }
    if (newThr == 0) {
        //如果新的容量上限=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桶的容量构造一个新的hash桶
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //遍历老的hash桶,把老的hash桶的数据重新hash放入到新的hashif (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    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;
                    }
                }
            }
        }
    }
    
    //返回新的hashreturn newTab;
}