概念

  • 元素(entry): 具体存储数据的东西
  • 桶(bucket): 存放元素的容器,可以是单个元素,可以是一个链表或者树
  • 容量(capacity): 可以放多少个桶
  • 负载因子(Load factor): 当元素的个数大于容量*负载因子(默认.75),那么需要扩容
  • 树化因子(TREEIFY_THRESHOLD): 当一个桶中的元素超过这个阈值时,会把链表转为红黑树;反过来还有一个反树化因子(UNTREEIFY_THRESHOLD),当桶中的元素少于这个阈值时,会把树转为链表

以上,其实应该没有中文名,我这为了叫着方便~ 如果不合适,请忍耐~ 毕竟中文才是最博大精深的语言~ 没有之一,不接受反驳~

方法

构造函数

HashMap一共提供了三个构造函数,区别主要在于是否要设定容量和阈值~
源码如下:

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public HashMap(int initialCapacity, float loadFactor) &#123; // 1
        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;
        this.threshold = tableSizeFor(initialCapacity); // 2
    &#125;

    /**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and the default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity.
     * @throws IllegalArgumentException if the initial capacity is negative.
     */
    public HashMap(int initialCapacity) &#123; // 3
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    &#125;

    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() &#123; // 4
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    &#125;
    
    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) &#123; // 5
        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;
    &#125;

说明:

  1. 带有两个参数的构造函数,可以指定容量和负载因子。
  2. 这个函数的作用是把容量修改到大于等于容量的最小的2的幂,见5
  3. 单参数构造函数,传入容量,使用默认的负载因子(.75)
  4. 无参构造函数,容量和负载因子均使用默认值(16, .75)
  5. 找到大于等于容量的最小的2的幂,为啥要把容量设定为2的幂数呢,见下文~

hash

先来看一下源码~

    /**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */
    static final int hash(Object key) &#123;
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    &#125;

hashCode()返回一个int型数据,然后这里是将这个hashCode的高16位和低16位进行了一个异或运算~
据说,这是一个可以在一定程度上减少冲突,并且运算简单的好方法~ 又便宜又好吃~
在下面,我们会看见index的计算方式: (n-1) & hash
n是当前的容量,那么如果当这个n比较小的时候,相当于只有hashCode低位参与了运算,而通过现在的hash函数,相当于把高位也引入到了计算当中~

put

重头戏来了,这应该是我们最关心的方法,还是先上代码~

/**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
    public V put(K key, V value) &#123; // 1
        return putVal(hash(key), key, value, false, true);
    &#125;
    /**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) &#123;
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0) // 2
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null) // 3
            tab[i] = newNode(hash, key, value, null);
        else &#123; // 4
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) // 5
                e = p;
            else if (p instanceof TreeNode) // 6
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else &#123; // 7
                for (int binCount = 0; ; ++binCount) &#123;
                    if ((e = p.next) == null) &#123;
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash); // 8
                        break;
                    &#125;
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                &#125;
            &#125;
            if (e != null) &#123; // existing mapping for key // 9
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            &#125;
        &#125;
        ++modCount;
        if (++size > threshold) // 10 
            resize(); 
        afterNodeInsertion(evict);
        return null;
    &#125;

说明:

  1. put方法,先计算keyhash值,然后调用putVal方法。
  2. 初始化table,在构造函数中,仅仅是指定了容量和负载因子,并没有初始化容器。
  3. 这里是,如果对应的索引位置没有元素,那么直接把新元素放到这里。同时可以看到,计算索引的方式是(n - 1) & hash,而不是取余之类的。
  4. 如果对应的索引位置,已经有元素了,那么执行下面的逻辑。
  5. 如果索引位置的当前元素pkey和新元素一样,那么用e指向这个元素(也就是我们要put进去的key,其实已经存在了)。
  6. 当前元素pkey和新元素不一样,同时p又是一个树节点,那么就要调用putTreeVal方法;同时,如果这棵树中,已经包含了新元素的key,那么用e指向他。
  7. 同上,不过这个不是树,而是链表的情况。
  8. 如果,链表的长度超过树化阈值(TREEIFY_THRESHOLD),那么就要把链表转化为红黑树
  9. 这里检查一下e,他表示的意思是,是否map中已经存在key对应的元素了。这里要根据onlyIfAbsent参数来决定是否用新元素替换老元素。
  10. 如果元素的数量超过了阈值(threshold),那么需要进行扩容(resize)。

这里需要注意的是,我们在双参数版本的构造函数中将threshold赋值为容量了,但是在这里的threshold其实是容量*负载因子,这是咋回事呢?
table的初始化是在第一次进行put操作时,调用resize方法完成的,而这个threshold的值,也是在resize方法中被重新赋值的。
详见下文~

resize

resize这个方法,有两个作用~
第一个就是初始化table时需要,另一个就是扩容的时候需要~
每次扩容后的容量都会变成之前的二倍~
扩容实际上是创建了一个新的table,然后还需要把数据迁移过来~
源码如下:

    /**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() &#123;
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) &#123; // 1
            if (oldCap >= MAXIMUM_CAPACITY) &#123; // 2
                threshold = Integer.MAX_VALUE;
                return oldTab;
            &#125;
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY) // 3
                newThr = oldThr << 1; // double threshold
        &#125;
        else if (oldThr > 0) // initial capacity was placed in threshold // 4
            newCap = oldThr;
        else &#123;               // zero initial threshold signifies using defaults // 5
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        &#125;
        if (newThr == 0) &#123; // 6
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        &#125;
        threshold = newThr; // 7
        @SuppressWarnings(&#123;"rawtypes","unchecked"&#125;)
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 8
        table = newTab;
        if (oldTab != null) &#123; 
            for (int j = 0; j < oldCap; ++j) &#123; // 9
                Node<K,V> e;
                if ((e = oldTab[j]) != null) &#123;
                    oldTab[j] = null;
                    if (e.next == null)  // 10
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode) // 11
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 
                    else &#123; // preserve order // 12
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do &#123;
                            next = e.next;
                            if ((e.hash & oldCap) == 0) &#123;
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            &#125;
                            else &#123;
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            &#125;
                        &#125; while ((e = next) != null);
                        if (loTail != null) &#123;
                            loTail.next = null;
                            newTab[j] = loHead;
                        &#125;
                        if (hiTail != null) &#123;
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        &#125;
                    &#125;
                &#125;
            &#125;
        &#125;
        return newTab;
    &#125;

说明:

  1. 这里是处理正常扩容的情况(非初始化)。
  2. 如果容量已经超过最大容量(MAXIMUM_CAPACITY)了,那就不能扩容了,并把扩容的阈值(threshold)设置为最大,即无法再触发resize方法。
  3. 正常扩容成原来的二倍,包括容量和阈值(threshold),因为这个最大容量值(MAXIMUM_CAPACITY)为1 << 30,所以这里并不会溢出。
  4. 这里是处理使用带参构造函数构造的情况。
  5. 这里是处理使用无参构造函数构造的情况,均使用默认值。
  6. 计算阈值(threshold)。
  7. 这里就是修改阈值(threshold)的地方,在构造函数中阈值等于容量,而在这里进行了重新赋值,赋值为容量*负载因子。
  8. 创建一个新的扩容后的table
  9. 将数据从老table中转移到新table
  10. table中对应位置的元素是普通元素,那么直接把它挪过去就好了。
  11. 如果原来的元素是一棵树,那么需要把树拆为两部分,原来索引的部分,和新索引的部分,如拆分后树的元素过少,还需要将树转为链表~
  12. 如果原来的元素是一个列表,那么也需要将他拆分为两部分,分别放在原来的索引,和新索引处。

补充:关于转移数据时候的索引:
因为HashMap的容量,永远是2的幂数,而扩容也是扩容到原来的2倍,那么从二进制上来讲,加入原来的容量是2,那么就是00000010,扩容后就变成了4,也就是00000100
那么,假如以前有两个key,分别是15,也就是0000000100000101,他们两个的索引都是1,而扩容后,他们的索引分别是15
其实,再举几个例子也是一样的,从老table到新table迁移数据的时候,只有两种情况,还在原索引位置,以及原索引位置+原容量。
仔细想想是不是这么一回事,因为扩容后,就是容量左移一位而已,再计算索引的时候,差别也仅仅是看新容量的高位与元素的高位进行与运算是否为1,差的正好也是原来的容量的值而已。
上面代码中,树也好,链表也好,其实都是根据这个原理来计算索引和迁移元素的。
(这也是在初始化时,会自动把容量转换为大于等于我们想要的容量的最小的2的幂所带来的一个很大的好处~)

get

get方法就很简单了,简单说就是计算hash值,然后去table里去取值~
源码如下:

    /**
     * Returns the value to which the specified key is mapped,
     * or &#123;@code null&#125; if this map contains no mapping for the key.
     *
     * <p>More formally, if this map contains a mapping from a key
     * &#123;@code k&#125; to a value &#123;@code v&#125; such that &#123;@code (key==null ? k==null :
     * key.equals(k))&#125;, then this method returns &#123;@code v&#125;; otherwise
     * it returns &#123;@code null&#125;.  (There can be at most one such mapping.)
     *
     * <p>A return value of &#123;@code null&#125; does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to &#123;@code null&#125;.
     * The &#123;@link #containsKey containsKey&#125; operation may be used to
     * distinguish these two cases.
     *
     * @see #put(Object, Object)
     */
    public V get(Object key) &#123;
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value; // 1
    &#125;

    /**
     * Implements Map.get and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @return the node, or null if none
     */
    final Node<K,V> getNode(int hash, Object key) &#123;
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) &#123;
            if (first.hash == hash && // always check first node // 2
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) &#123;
                if (first instanceof TreeNode) // 3
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do &#123; // 4
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                &#125; while ((e = e.next) != null);
            &#125;
        &#125;
        return null;
    &#125;

说明:

  1. 计算keyhash,然后调用getNode方法。
  2. 计算索引并判断对应位置是否有元素,有的话那第一个元素(first)是不是我们要拿的元素(比较key)。
  3. 如果第一个元素(first)不对的话,分两种情况,如果第一个元素是树节点,那么就调用getTreeNode方法。
  4. 接上面,如果不是树节点,那就是链表,遍历链表找我们要的元素~

关于多线程

HashMap不是线程安全的,所以在多线程下使用就会出现一些问题~ 比如死循环,比如元素丢失问题等~
(人家本来就不是线程安全的,非用它干啥呀?生活本该简简单单~ 简直就是没事找事~)

死循环

这个问题好像是一个很经典的问题~不过仅限于1.8之前的版本~
1.8版本之后,把头插改为了尾插,就把这个问题解决了,但是,好像这个问题太经典了,老是出现在各个地方,无处可逃哇~

原因:
1.7版本的相关代码如下:

void transfer(Entry[] newTable) &#123;
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) &#123;
        Entry e = src[j];
        if (e != null) &#123;
            src[j] = null;
            do &#123;
                Entry next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            &#125; while (e != null);
        &#125;
    &#125;
&#125;

transfer方法是在resize后调用的,作用是迁移数据~
我们可以看他这个链表部分,是在遍历过程中,给倒了个顺序(头插)~
所以,假设本来table[1]处的链表为A -> B -> C,那么正常resize后,会变成C -> B -> A(假设恰好没有元素的索引发生变化)。
但是在多线程的情况下,可能会同时触发resize方法~
那么假设有两个线程:Thread1Thread2,以及一个HashMap,如下所示~

-----
| 0 | -> 0
| 1 | -> 3 -> 7 -> 1
-----

这时触发了resize,则新的table应该长这样:

-----
| 0 | -> 0
| 1 | -> 1
| 2 |
| 3 | -> 7 -> 3
-----

然而,在执行过程中,当Thread1执行到table[1] : 3 -> null,准备处理7结点时,没有抢占到CPU。
而这时Thread2抢到了,开始执行,并一直执行完成。
这时Thread1继续执行,因为Thread2已经执行完成,并且把最新的数据刷回主存了,所以Thread1会得到最新的结果。
当他执行到table[1] : 3 -> 7 -> null后,需要继续处理下一个结点,但是这个下一个结点本来是null,但是却变成了3
这个时候,新HashMap就变成了这样:

-----
| 0 | -> 0
| 1 | -> 1
| 2 |
| 3 | -> 3 <-> 7 
-----

而,这时,如果get(3), get(7),还是不会发生任何问题,但是如果get(11),那么就会现在table[3]这个环中,无法自拔。死循环就产生了~

注:
虽然这也只是偶然现象,但是 好像有这么一句话:任何在多线程下可能发生的错误场景最终一定会发生。

而在1.8以及之后的版本中,不管是处理红黑树,还是链表,都保证原来的顺序~ 就避免了这个问题~
源码如下:

    final Node<K,V>[] resize() &#123;
        ......
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 1
                    else &#123; // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do &#123; // 2
                            next = e.next;
                            if ((e.hash & oldCap) == 0) &#123;
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            &#125;
                            else &#123;
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            &#125;
                        &#125; while ((e = next) != null);
                        if (loTail != null) &#123;
                            loTail.next = null;
                            newTab[j] = loHead;
                        &#125;
                        if (hiTail != null) &#123;
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        &#125;
                    &#125;
        ......
    &#125;

说明:

  1. 处理树的过程,见下面代码~
  2. 链表的处理过程,因为迁移时索引只有两种情况,还是原索引不变(低位),原索引+原容量(高位),所以代码中搞了两个链表,分别对应低位和高位,并保证了原来的顺序不变~
        /**
         * Splits nodes in a tree bin into lower and upper tree bins,
         * or untreeifies if now too small. Called only from resize;
         * see above discussion about split bits and indices.
         *
         * @param map the map
         * @param tab the table for recording bin heads
         * @param index the index of the table being split
         * @param bit the bit of hash to split on
         */
        final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) &#123;
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
            for (TreeNode<K,V> e = b, next; e != null; e = next) &#123;
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                if ((e.hash & bit) == 0) &#123;
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                &#125;
                else &#123;
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                &#125;
            &#125;

            if (loHead != null) &#123;
                if (lc <= UNTREEIFY_THRESHOLD)
                    tab[index] = loHead.untreeify(map);
                else &#123;
                    tab[index] = loHead;
                    if (hiHead != null) // (else is already treeified)
                        loHead.treeify(tab);
                &#125;
            &#125;
            if (hiHead != null) &#123;
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else &#123;
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                &#125;
            &#125;
        &#125;

说明:
其实跟链表大同小异,核心就是把之前一个位置的元素按照索引分成了两个部分,并保证了顺序~

丢失元素

至于丢失元素这个问题,就很简单了,HashMap并没有去解决这个问题,毕竟本身就不是线程安全的~
简单提一嘴就是,在putVal方法中(见下面代码)~
假设两个线程同时执行到1处,然后第一个线程执行完之后,第二个线程得到了CPU开始执行,那么tab[i]的值就是第二个线程的元素了~
第一个线程的元素被覆盖掉了~
如果要解决这个问题,请使用ConcurrentHashMap~

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) &#123;
        ......
        if ((p = tab[i = (n - 1) & hash]) == null) 
            tab[i] = newNode(hash, key, value, null); // 1
        ......
    &#125;

参考

Java-HashMap工作原理及实现
并发时的问题1
并发时的问题2
面试问题