Java集合(7)——源码剖析(4)——HashMap源码分析

目录

1.概述

2.JDK1.7源码分析

2.1 底层存储的对象

2.2 成员变量

2.3 构造函数

2.4 添加键值对put

2.5 从key到数组索引值的计算

2.6 获取键值对get

2.7 扩容resize

2.8 jdk1.7中HashMap多线程下扩容死锁演示与分析

3.jdk1.8源码分析

3.1 底层存储的对象

3.2 成员变量

3.3 构造函数

3.4 添加键值对put

3.5 将链表转换为红黑树的方法treeifyBin

3.6 获取键值对get

3.7 扩容方法resize

3.8 为什么要在链表的长度刚好达到8以后就把它转成红黑树?

泊松分布

4.jdk1.8与jdk1.7相比的变化总结

5.HashMap实现中对组合模式的应用

6.HashMap中常用方法使用


1.概述

  • HashMap 主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。
  • JDK1.8 之前 HashMap 由 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).
  • JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于8时,将链表转化为红黑树,以减少搜索时间。

2.JDK1.7源码分析

  • JDK1.7HashMap 底层是数组和链表 结合在一起使用也就是链表散列。

2.1 底层存储的对象

HashMap1.7底层数组中存储的是Entry<key,value>对象

    static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;  //键
        V value;      //值
        Entry<K,V> next;   //指向的下一个节点 
        int hash;    //哈希值

        /**
         * 构造一个Entry节点
         */
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }

        public final K getKey() {
            return key;
        }

        public final V getValue() {
            return value;
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry e = (Map.Entry)o;
            Object k1 = getKey();
            Object k2 = e.getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = getValue();
                Object v2 = e.getValue();
                if (v1 == v2 || (v1 != null && v1.equals(v2)))
                    return true;
            }
            return false;
        }

        public final int hashCode() {
            return (key==null   ? 0 : key.hashCode()) ^
                   (value==null ? 0 : value.hashCode());
        }

        public final String toString() {
            return getKey() + "=" + getValue();
        }

        /**
         * This method is invoked whenever the value in an entry is
         * overwritten by an invocation of put(k,v) for a key k that's already
         * in the HashMap.
         */
        void recordAccess(HashMap<K,V> m) {
        }

        /**
         * This method is invoked whenever the entry is
         * removed from the table.
         */
        void recordRemoval(HashMap<K,V> m) {
        }
    }

2.2 成员变量

public class HashMap<K,V>
        extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable {

    /**
     * 默认容量,即默认的数组的大小
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //16

    /**
     * 数组最大容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认的加载因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 一个空的数组实例常量,
     * 类似于ArrayList中的   private static final Object[] EMPTY_ELEMENTDATA = {};
     * <p>
     * 用于在初始化HashMap的时候赋予它一个空数组
     */
    static final Entry<?, ?>[] EMPTY_TABLE = {};

    /**
     * 真正存储键值对的数组,
     * <p>
     * 准确的说,数组中元素存放的是它每一个位置对应的链表的头节点
     * <p>
     * 默认初始化为一个空数组
     */
    transient Entry<K, V>[] table = (Entry<K, V>[]) EMPTY_TABLE;

    /**
     * HashMap中的键值对的个数
     */
    transient int size;

    /**
     * 扩容阈值
     *
     * 大多数情况下threshold = capacity * load factor(除过数组的容量已经扩充到很大的时候,具体见下文代码)
     *
     * 如果数组table是空数组EMPTY_TABLE,并且扩容阈值threshold=inialCapacity,
     * 那么数组会在函数inflated中被创建
     *
     */
    int threshold;

    /**
     * 加载因子
     */
    final float loadFactor;

    /**
     * HashMap被操作的数量
     */
    transient int modCount;

    /**
     * 
     */
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

    /**
     * A randomizing value associated with this instance that is applied to
     * hash code of keys to make hash collisions harder to find. If 0 then
     * alternative hashing is disabled.
     */
    transient int hashSeed = 0;
}
  • loadFactor 加载因子
  • 问题:为什么HashMap的加载因子要设置在0.75?
    • 加载因子是表示Hsah表中元素的填满的程度。
    • 加载因子越大,填满的元素越多,空间利用率越高,但冲突的机会加大了。也就是链表中的长度就会增加
    • 反之,加载因子越小,填满的元素越少,冲突的机会减小,但空间浪费多了。
    • 冲突的机会越大,则查找的成本越高。反之,查找的成本越小。
    • 因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷。而loadFactor的默认值为0.75f是官方给出的一个比较好的临界值。
  • 给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能。所以使用HashMap时尽量预估自己的数据量来设置初始值。
  • 扩容阈值threshold
    • threshold = capacity * loadFactor当size>=threshold的时候,那么就要考虑对数组的扩容了,也就是说,扩容阈值的意思就是衡量数组是否需要扩容的一个标准

2.3 构造函数

    /**
     * 传入自己的容量和加载因子的构造函数
     *
     * 实际上函数内除过对我们输入参数做了范围检查,
     * 只是将我们输入的加载因子赋值给成员变量loadFactor
     * 将我们传入的初始容量赋值给threshold,
     * 而这时threshold这个值是为了在put方法中调用inflateTable方法初始化HashMap的数组的时候
     * 传入该参数来计算数组的容量的
     *
     *
     */
    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;
        threshold = initialCapacity;
        /**
         * 空方法,HashMap中并没有实现它
         */
        init();
    }

    /**
     * 我们自己只传入初始容量,加载因子采用默认的0.75
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 初始容量采用默认值16,加载因子采用默认的0.75
     */
    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 通过已经存在的Map创建HashMap
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        /**
         * 构造一个HashMap
         * 容量为m.size() / DEFAULT_LOAD_FACTOR) + 1与16中的最大值
         * 加载因子为0.75
         */
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);

        /**
         * 根据容量创建数组
         */
        inflateTable(threshold);

        /**
         * 将m数组中的
         */
        putAllForCreate(m);
    }

    /**
     * 遍历m,将它的键值对放入到我们的新创建的数组中去
     * 
     */
    private void putAllForCreate(Map<? extends K, ? extends V> m) {
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            putForCreate(e.getKey(), e.getValue());
    }

    /**
     * This method is used instead of put by constructors and
     * pseudoconstructors (clone, readObject).  It does not resize the table,
     * check for comodification, etc.  It calls createEntry rather than
     * addEntry.
     */
    private void putForCreate(K key, V value) {
        //传入的键值对的hash值
        int hash = null == key ? 0 : hash(key);
        //计算传入的键值对所对应的数组索引值
        int i = indexFor(hash, table.length);

        /**
         * 遍历该数组索引位置处的链表,判断是否该键已经存在,存在的话直接返回
         */
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k)))) {
                e.value = value;
                return;
            }
        }
        /**
         * 创建一个新的节点插入到数组索引位置的链表中(头插法)
         */
        createEntry(hash, key, value, i);
    }

    /**
     * 创建一个新的键值对,并采用头插法插入到数组索引值为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++;
    }

总结:

  • jdk1.7中HashMap在初始化的时候,和ArrayList一样,采用的是延迟创建在构造方法中只是初始化loadFactor和threshold,而实际数组空间的创建是在put方法中

2.4 添加键值对put

使用示例:

package HashMapKeyDemo;

import java.util.HashMap;

public class HashMapTest {
    public static void main(String[] args) {
        HashMap<String,String> map= new HashMap<>();

        map.put("1","2");
        //由于已经存在该key,则会覆盖原来的值,覆盖后会返回旧值
        String value = map.put("1","3");
        System.out.println(value);
        System.out.println(map.get("1"));
    }
}

简单讲解put的思路:

  • int hashCode = key.hashCode()       //根据key得到一个hashCode
  • int index = hashCode % table.length    //根据hashCode取余计算出数组的索引值
  • table[index] = new Entry(key,value,table[index])    //将key,value封装成一个节点,插入到该数组该索引位置的头节点的位置,原来头节点就是table[index],所以让新节点的next域指向原来的头节点table[index],然后让头节点指向新节点,即完成了头部的插入

源码分析:

    /**
     * 插入
     */
    public V put(K key, V value) {
        /**
         * HashMap创建第一次put,会进入此判断,初始化数组
         */
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }

        /**
         * 对于我们插入的key为null的情况,做特殊处理
         *
         * 固定插入到数组的索引为0处的链表中
         */
        if (key == null)
            return putForNullKey(value);
        /**
         * key不为null的话
         */
        //求出key对应的hash值
        int hash = hash(key);
        //根据hash值和数组长度求出key数组的索引值
        int i = indexFor(hash, table.length);
        //遍历该索引处的链表
        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))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //该键不存在该链表中,头插法插入到该链表中
        addEntry(hash, key, value, i);
        return null;
    }

    /**
     * 初始化数组
     */
    private void inflateTable(int toSize) {

        // 找出大于等于toSize的最小的2的次方数
        // 保证容量不管我们传入多少的容量值,都能保证数组容量为2的幂次方
        int capacity = roundUpToPowerOf2(toSize);
        // 计算新的扩容阈值
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //创建新的容量的数组
        table = new Entry[capacity];
        //计算新的hashSeed
        initHashSeedAsNeeded(capacity);
    }

    /**
     * 传入容量比最大容量还大,就为最大容量
     * 传入容量等于1,就为1,即2的0次方
     * 否则,返回一个大于等于number的最小的2的次方数
     *
     */
    private static int roundUpToPowerOf2(int number) {

        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

    /**
     * 此方法专门对于key为null的情况做出处理
     */
    private V putForNullKey(V value) {

        /**
         * 遍历数组0位置的链表,查找它当中是否已经有以null为key的键值对
         * 有的话,覆盖它的value,直接返回
         * 循环完都没有的话,才会执行到循环后面的addEntry
         */
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        /**
         * 将key=null,value=value的键值对插入到数组的第一个位置的链表中(头插法)
         */
        addEntry(0, null, value, 0);
        return null;
    }

    /**
     * 将键值对采用头插法插入到数组对应位置的链表中
     */
    void addEntry(int hash, K key, V value, int bucketIndex) {
        /**
         * 元素个数size大于扩容阈值threshold并且要插入的数组索引位置不为null的话,进行扩容
         *
         */
        if ((size >= threshold) && (null != table[bucketIndex])) {
            //将数组扩容为原来的两倍
            resize(2 * table.length);
            //这时需要重新计算hash值,因为jdk1.7中hash函数中用到了hashSeed,
            // 而hashSeed在resize扩容时发生了变化,所以需要重新计算hash
            hash = (null != key) ? hash(key) : 0;
            //重新计算数组的索引值
            bucketIndex = indexFor(hash, table.length);
        }
        //创建Entry并通过头插法插入到数组的bucketIndex处的链表中去
        createEntry(hash, key, value, bucketIndex);
    }

总结:

  • 1.通过上述源码可以发现实际在jdk1.7中扩容的条件并不只是HashMap中的元素个数size大于扩容阈值threshold,而是有两个条件:
    • 1.HashMap中的元素个数size大于扩容阈值threshold
    • 2.要插入的键值对中key对应的数组索引位置不为null
  • 2.jdk1.7的对扩容方法的调用实际是在addEntry方法
  • 3.jdk1.7中对于Entry节点的插入采用头插法
  • 4.不管我们传入的初始容量是多少,最终都会被转化为2的幂次方,至于原因见下文

2.5 从key到数组索引值的计算

问题1:为什么HashMap在初始化的时候的容量要被转化为2的幂次方?

我们先来看一下HashMap的实现中是如何计算数组的地址值的,

  • 1.首先,每个key对象都有自己的hashCode方法,会返回一个32位int的hashCode,
  • 2.然后hashCode通过扰动函数(即HashMap的hash(Object)方法)得到一个处理后的hash值
/**
 *  jdk1.7 的扰动函数
 */

final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
  /**
   *  jdk1.8 的扰动函数
   */
  static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }
  • 扰动函数的作用见下文问题2
  • 3.通过该hash值然后计算数组的索引地址

jdk1.7的地址值计算:

jdk1.8的地址值计算:

可以发现,在jdk1.7和jdk1.8中,通过hash值计算数组的索引值的方式是相同的,都是

index = (table.length-1) & hash;

我们接下来通过示例来分析以下这个计算方法:

HashMap的容量我们就以HashMap的默认容量为16为例吧,hash值我们就随机找一个数字吧

table.length:

  • 16:0000_0000 0000_0000  0000_0000 0001_0000 

table.length - 1:

  • 15:0000_0000 0000_0000  0000_0000 0000_1111

hash:

  • 0110_1001 0010_0001  0000_0010 0000_0111

(table.length - 1)& hash:

  • 0000_0000 0000_0000  0000_0000 0000_1111
  • &                                                                           每一位相与,都为1时得1,否则得0
  • 0110_1001 0010_0001  0000_0010 0000_0111
  • 0000_0000 0000_0000  0000_0000 0000_0111
  • 可以发现结果很特殊,table.length只要是2的幂次方,然后减1的话,它的前面的高位都会变成全0,后面均为1,而后面的位数能表示的范围正好是[0,{\color{Red} 2^{n}}-1],即刚好是数组的索引范围,相与后,左边前32-n位都为0,右边n位的结果是hash的对应的n位的值(如示例中就是hash的后4位0111,即最终的索引值计算出来为7)

总结:

  • 所以HashMap在初始化的时候的容量必须是2的幂次方是为这里计算数组的索引做铺垫,确保最终能通过与运算来计算数组的索引值
  • 其实在jdk早期实现中,数组的索引值的计算是通过取模来完成,但是在一些平台上运行效率比较慢,由于位运算比四则和取模运算的效率要快,所以此处采用这种与操作计算数组的索引值效率更快
  • 其实在jdk1.8中2的幂次方还有另外i一个作用,见下文jdk1.8源码的扩容讲解

问题2:为什么在key自身得到hashCode之后还要通过hash()进行右移处理?

  • 1.通过上述我们可以看到,如果不右移,其实最终真正参与到决定数组索引值的是数组的低n-1位,而其他高位对结果没有任何影响,通过右移将高位移到了低位,让高位参与到数组索引值的计算当中,防止更多的碰撞
  • 2.如果我们自身的hashCode函数写得不好产生很多碰撞(即按照我们hashCode函数返回的值直接计算数组索引值有许多相同时),通过这样的右移操作能减少这种碰撞

2.6 获取键值对get

    /**
     * 根据key查询值,
     */
    public V get(Object key) {
        //对key为null用专门的函数去获取
        if (key == null)
            return getForNullKey();
        
        Entry<K,V> entry = getEntry(key);
        //返回获取到的值
        return null == entry ? null : entry.getValue();
    }

    /**
     * 获取key为null的函数的值
     */
    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        
        //遍历数组0位置处的链表,寻找key为null的值
        // (因为key为null的键值对,在put的时候就确定被放置在table[0]中的链表中)
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

    /**
     * 获取非null的key对应的值
     */
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        //计算key对应的hash值
        int hash = (key == null) ? 0 : hash(key);
        //遍历key对应的数组索引处的链表
        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;
        }
        //未找到该键就返回null
        return null;
    }

2.7 扩容resize

    /**
     * 扩容核心方法
     */
    void resize(int newCapacity) {
        //先保存原始的数组
        Entry[] oldTable = table;
        //获取原始数组的长度
        int oldCapacity = oldTable.length;
        //判断原始数组是否达到最大容量
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }
        //创建新的容量的数组
        Entry[] newTable = new Entry[newCapacity];
        //将原始数组中的数据转移到新的数组中去
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        //将新的数组赋值给当前HashMap对象的table数组引用
        table = newTable;
        //计算新的扩容阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    /**
     * 将原始数组中的数据转移到新的数组中去
     */
    void transfer(Entry[] newTable, boolean rehash) {
        //获取新数组的长度,为了后面计算数组索引值做准备
        int newCapacity = newTable.length;
        //遍历原始的数组,此时每个数组位置存放的是一个链表
        for (Entry<K,V> e : table) {
            //遍历链表
            while(null != e) {
                //保存下一个节点
                Entry<K,V> next = e.next;
                //判断是否需要再次hash,对于一些节点再哈希,会将它移动到其他的桶当中,这样也可以将原来的链表的长度减少
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //计算数组的索引
                int i = indexFor(e.hash, newCapacity);
                //将当前节点插入到新数组对应索引位置的链表头部
                e.next = newTable[i];
                newTable[i] = e;
                //迭代
                e = next;
            }
        }
    }
  • 可以发现,在扩容时转移元素的过程中,数组中每个链表的节点的顺序发生了反转

2.8 jdk1.7中HashMap多线程下扩容死锁演示与分析

扩容在多线程下产生死锁的代码块如下:

其实以上代码while循环中是对数组中某一索引位置中链表的遍历转移,我们简单使用如下的图解进行演示死锁(此处只把数组画了2个只供演示死锁,真实情况数组的容量并非2个哟)

图解:

  • 1.假设两个线程t1和t2都执行到while循环当中,如下,它们在循环中都会有临时变量e指向的是当前节点,临时变量next指向下一个节点,我们姑且对线程t1用e1,next1,对线程t2用e2,next2来表示它们,并且开始e1和e2都指向,空白即为null

  • 2.当线程t1先执行到发生阻塞

  • 3.此时线程t2开始执行,执行了第一轮循环

  • 4.线程t2执行了第二轮循环

t2执行完毕

  • 5.此时线程t1继续执行完一轮循环

  • 6.线程t1执行第二轮循环

  • 7.线程t1执行第三轮循环

可以发现此时便形成了环,形成了死锁,而死锁的形成的原因就是因为在扩容时转移元素的过程中,数组中每个链表的节点的顺序发生了反转

3.jdk1.8源码分析

JDK1.8HashMap 底层是数组+链表+红黑树

3.1 底层存储的对象

jdk1.8底层存储的是Node节点,当链表长度小于8的时候,数组中存放的是Node本身,但是当转变为红黑树的时候,数组中存放的是Node的孙子类节点TreeNode

链表节点:Node节点

    /**
     * jdk1.8中HashMap底层存放的对象是Node
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;  //哈希值
        final K key;     //键
        V value;         //值
        Node<K,V> next;  //下一个节点

        //构造函数
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                        Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

红黑树节点:TreeNode节点

说明:由于红黑树节点的源码中有大量的操作,此处只截取了源码的一部分,关于红黑树原理还有在此类中详细源码实现解析,见我的另一篇博客:待补充

    /**
     * 红黑树节点
     *
     * 其实LinkedHashMap.Entry<K,V>是继承自HashMap.Node的
     *
     * static class Entry<K,V> extends HashMap.Node<K,V> {
     *
     *
     */
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K, V> parent;  //父节点
        TreeNode<K, V> left;    //左节点
        TreeNode<K, V> right;   //右节点
        TreeNode<K, V> prev;    // needed to unlink next upon deletion
        boolean red;            //节点的颜色

        TreeNode(int hash, K key, V val, Node<K, V> next) {
            super(hash, key, val, next);
        }

        /**
         * 返回红黑树的根节点
         */
        final TreeNode<K, V> root() {
            for (TreeNode<K, V> r = this, p; ; ) {
                if ((p = r.parent) == null)
                    return r;
                r = p;
            }
        }
    }

3.2 成员变量

public class HashMap<K,V>
        extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable {
    /**
     * 序列号
     */
    private static final long serialVersionUID = 362498820763181265L;

    /**
     * 默认容量
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 最大容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认加载因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 当桶(bucket)上的结点数大于这个值时会转成红黑树
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 当桶(bucket)上的结点数小于这个值时树转链表
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 桶中结构转化为红黑树对应的table的最小大小
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

    /**
     * 存储元素的数组,总是2的幂次倍
     *
     * 通过此也可以看到jdk1.8之后table存放的是Node
     */
    transient Node<K,V>[] table;

    /**
     * 存放具体元素的集合
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * HashMap中键值对的数量
     */
    transient int size;

    /**
     * HashMap被操作的数量
     */
    transient int modCount;

    /**
     * 扩容阈值
     */
    int threshold;

    /**
     * 加载因子
     */
    final float loadFactor;

}

3.3 构造函数

    /**
     * 与jdk1.7中差不多,只是将计算2的幂次方的容量提前到了此方法
     */
    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;
        this.threshold = tableSizeFor(initialCapacity);
    }

    /**
     * 同jdk1.7
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * 只赋予加载因子,未对容量进行初始化
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }


    /**
     * 使用已存在的Map构造新的HashMap
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

    /**
     * 遍历Map中的Entry对象的键值对,存放到当前的HashMap对象的数组中
     */
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            // 判断table是否已经初始化
            if (table == null) { // pre-size
                // 未初始化,s为m的实际元素个数
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                        (int)ft : MAXIMUM_CAPACITY);
                // 计算得到的t大于阈值,则初始化阈值
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            // 已初始化,并且m元素个数大于阈值,进行扩容处理
            else if (s > threshold)
                resize();
            // 将m中的所有元素添加至HashMap中
            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);
            }
        }
    }

3.4 添加键值对put

    /**
     * 提供给外界的put方法
     *
     * key存在返回的时覆盖的旧值
     *    不存在的话返回的是null
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * put核心方法
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //定义一个临时的数组引用用于接收原始数组
        Node<K,V>[] tab;
        //定义一个临时节点
        Node<K,V> p;
        //定义一个临时变量n接收数组的长度,临时变量i接收计算后的数组索引值
        int n, i;

        /**
         * 第一次put的时候,数组进行初始化
         *
         * jdk1.8将初始化数组的方法和扩容方法结合在了一起为resize()
         */
        if ((tab = table) == null || (n = tab.length) == 0)
            //此处使用临时数组引用tab接收了初始化后的数组,临时变量n接收了初始化数组后,数组的长度
            n = (tab = resize()).length;


        /**
         * 如果key对应的数组索引位置为没有任何键值对,即为null,就新建一个节点(即新建该位置链表头节点),放置在该位置
         */
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //该数组索引处已经有键值对存在
        else {
            Node<K,V> e; K k;

            //如果数组该索引位置处存放的第一个Node直接是该key对应的键值对,则直接覆盖该处的值
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果数组该索引位置处存放的第一个节点是红黑树节点,直接使用红黑树结点的putTreeVal方法进行put
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //否则,数组该索引位置处存放的就是一个长度小于等于8的链表
            else {
                //遍历数组该索引位置处的链表
                for (int binCount = 0; ; ++binCount) {
                    //如果循环到链表尾节点
                    if ((e = p.next) == null) {
                        //将新节点插入链表的尾部,通过这种方式使链表的节点不会发生反转
                        p.next = newNode(hash, key, value, null);
                        //如果这循环第8次,表明此时插入的节点为第9个节点,即链表的节点个数超过了8,
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //将链表转换为红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    //判断当前节点e是否为key对应的键值对,是的话就退出循环
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //迭代
                    p = e;
                }
            }
            //当我们在原来的链表中找到了该key对应的键值对,直接覆盖该key对应的值,并且返回该值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //在找到对应的key之后进行覆盖之后做一些事情,这里默认也为空实现,我们可以重写该方法
                afterNodeAccess(e);
                //返回原始值
                return oldValue;
            }
        }
        ++modCount;
        //如果新增添加了一个节点后,HashMap中键值对的个数大于扩容阈值的话,进行扩容
        if (++size > threshold)
            //扩容
            resize();
        //在插入一个节点后做一些事情,这里是空实现,我们可以重写去实现
        afterNodeInsertion(evict);
        //如果是插入节点,返回的是null
        return null;
    }

总结:

  • 1.在jdk1.8中扩容是在新添加了节点之后进行判断,并且判断只有一个条件即新增节点后的HashMap的长度大于阈值
  • 2.jdk1.8的对扩容方法的调用就在putVal中
  • 3.jdk1.8中对于链表中Node节点的插入采用尾插法
  • 4.链表的长度大于8的时候,将链表节点转化为红黑树来提高查询效率,从O(n)提升到O(lgn)

3.5 将链表转换为红黑树的方法treeifyBin

待补充

3.6 获取键值对get

    /**
     * 提供给用户调用的根据键获取值的方法
     * 
     * 存在该键对应的节点,就返回值
     * 不存在就返回null
     */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    /**
     * 获取键对应的节点
     */
    final Node<K,V> getNode(int hash, Object key) {
        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) {
            //数组第一个索引位置处的头节点是就是该key对应的键值对,就直接返回
            if (first.hash == hash && 
                    ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //如果头节点后存在下一个节点
            if ((e = first.next) != null) {
                //头节点是红黑树节点,直接调用它的getTreeNode查找
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //数组该索引位置是链表,循环查找该key对应的键值对
                do {
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

3.7 扩容方法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-else判断仅是通过原始容量和扩容阈值
         * 得到新的容量和扩容阈值
         */
        //原始容量大于0,代表原始数组已经经过了初始化
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //扩容为原来两倍(左移1位即乘以2)
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        /**
         *  初始化数组的
         *  原始数组容量为0,扩容阈值大于0
         *
         *  即HashMap中使用以下两个函数构造
         *     public HashMap(int initialCapacity, float loadFactor)
         *     public HashMap(int initialCapacity)
         */
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;

        /**
         * 原始数组容量为0,扩容阈值也为0
         *
         * 即HashMap中使用默认函数构造
         *   public HashMap()
         *
         */
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }


        /**
         * 如果新的扩容阈值仍然为0,计算新的扩容阈值
         */
        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"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        /**
         * 原始数组是已经经过初始化的话
         */
        if (oldTab != null) {
            //遍历原始数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                //原始数组索引位置j处有节点
                if ((e = oldTab[j]) != null) {
                    //将原始数组该索引位置置空,以让GC来回收
                    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);
                    /**
                     * 数组中存放的是长度大于1,小于等于8的链表
                     */
                    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;
                            /**
                             * e.hash & oldCap的解释见代码后
                             *
                             * 通过e.hash & oldCap的结果将原始的链表划分为高低位两个链表,
                             * 低位存放到新数组的相同位置,高位链表存放到相同位置+原始容量的位置
                             *
                             * 通过上述将链表进行了拆分,将数组变得松散,
                             * 并且不至于让数组一半空间空置
                             *
                             * 避免了rehash,
                             *
                             * e.hash & (oldCap + oldCap-1)
                             *
                             */
                            //结果为0
                            if ((e.hash & oldCap) == 0) {
                                //将当前节点插入低位链表(尾插法)
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            //结果非0
                            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;
    }

以下分析e.hash & oldCap这极为精妙的位运算的设计:

在jdk1.8中HashMap在初始化的时候的容量要被转化为2的幂次方的另一个原因:

oldCap

  • 16:0000_0000 0000_0000  0000_0000 0001_0000 

e.hash:

  • 0110_1001 0010_0001  0000_0010 0000_0111

e.hash & oldCap:

  • 0110_1001 0010_0001  0000_0010 0000_0111
  • &                                                                           每一位相与,都为1时得1,否则得0
  • 0000_0000 0000_0000  0000_0000 0001_0000 
  • 0000_0000 0000_0000  0000_0000 0000_0000
  • 可以发现上述保证容量为2的n次方,只有n+1位影响结果,该式计算下来只有两种结果,要么为0,要么就为原始容量
  • 所以通过就可以通过if-else来区分高低位链表

问题:为什么我们将高位链表的节点存放到原始数组相同索引位置+原始容量()的索引位置后不用重新计算hash值?

  • 通过上述我们可以知道能加入高位链表的都是hash值的n+1位为1
  • 当我们再次通过key计算数组索引值的时候,是通过n&(newCap-1)来计算的,而此时newCap实际就是2oldCap
  • 我们再用具体数值来举例演示:

oldCap

  • 16:0000_0000 0000_0000  0000_0000 0001_0000 

e.hash:(此处由于if-else的判断中对e.hash & oldCap保证了高位链表中节点的hash值的n+1位为1)

  • 0110_1001 0010_0001  0000_0010 0001_0111

newCap

  • 32:0000_0000 0000_0000  0000_0000 0010_0000 

newCap-1:(通过+oldCap,刚好保证了newCap-1的第n+1位也为1)

  • 32:0000_0000 0000_0000  0000_0000 0001_1111 

e.hash & (newCap-1):(所以就保证了它们相与的结果第n+1为必为1)

  • 0110_1001 0010_0001  0000_0010 0001_0111
  • &                                                                           每一位相与,都为1时得1,否则得0
  • 0000_0000 0000_0000  0000_0000 0001_1111 
  • 0000_0000 0000_0000  0000_0000 0001_0111
  • 所以刚好计算结果就是原始数组相同索引位置+原始容量,从而避免rehash

3.8 为什么要在链表的长度刚好达到8以后就把它转成红黑树?

问题:为什么要在链表的长度刚好达到8以后就把它转成红黑树?

  • 因为当我们put一个键值对的时候,产生哈希碰撞,放在同一个桶里的概率服从泊松分布
  • 在理想情况下,使用随机哈希码,节点出现的频率在hash桶中遵循泊松分布,同时上面给出了桶中元素个数和概率的对照表。从上面的表中可以看到当桶中元素到达8个的时候,概率已经变得非常小,也就是说用0.75作为加载因子,每个碰撞位置的链表长度超过8个的概率是很小的

  • 虽然很小,但在数据很大的时候,还是会发生,转化为红黑树能够提高查询效率,但由于转化为红黑树的概率是很小的,实际上jdk1.8比jdk1.7的HashMap的性能上只提高了8%-10%

关于泊松分布的解释如下

说明:此处使用了此链接中的解释http://www.ruanyifeng.com/blog/2015/06/poisson-distribution.html#comment-356111

泊松分布

日常生活中,大量事件是有固定频率的。

  • 某医院平均每小时出生3个婴儿
  • 某公司平均每10分钟接到1个电话
  • 某超市平均每天销售4包xx牌奶粉
  • 某网站平均每分钟有2次访问

它们的特点就是,我们可以预估这些事件的总数,但是没法知道具体的发生时间。已知平均每小时出生3个婴儿,请问下一个小时,会出生几个?

有可能一下子出生6个,也有可能一个都不出生。这是我们没法知道的。

泊松分布就是描述某段时间内,事件具体的发生概率。

上面就是泊松分布的公式。

等号的左边,

  • P 表示概率,
  • N表示某种函数关系,
  • t 表示时间,
  • n 表示数量,
  • 1小时内出生3个婴儿的概率,就表示为 P(N(1) = 3) 。

等号的右边,

  • λ 表示事件的频率。

接下来两个小时,一个婴儿都不出生的概率是0.25%,基本不可能发生。

接下来一个小时,至少出生两个婴儿的概率是80%。

泊松分布的图形大概是下面的样子。

可以看到,在频率附近,事件的发生概率最高,然后向两边对称下降,即变得越大和越小都不太可能。每小时出生3个婴儿,这是最可能的结果,出生得越多或越少,就越不可能。

一句话总结:泊松分布是单位时间内独立事件发生次数的概率分布

请注意是"独立事件",泊松分布的前提是,事件之间不能有关联,否则就不能运用上面的公式。

4.jdk1.8与jdk1.7相比的变化总结

变化1:

  • jdk1.8在构造函数中就完成了对容量转化为2的幂次方,通过tableSizeFor方法来计算
  • jdk1.7是在第一次put的时候调用inflateTable方法时,在其中通过roundUpToPowerOf2方法来计算

变化2:

  • jdk1.7默认构造函数会被赋予数组默认容量和默认加载因子,并且此时数组引用table指向空数组
  • jdk1.8默认构造函数只会赋予默认加载因子,并且此时数组引用table为null

变化3:

  • jdk1.7中HashMap在创建后在第一次初始化数组是通过inflateTable方法,扩容通过resize()方法
  • jdk1.8将初始化数组的方法和扩容方法结合在了一起为resize()

变化4:

  • jdk1.7中链表的节点采用头插法
  • jdk1.8中链表的节点采用尾插法

变化5:

  • jdk1.7中在插入节点以前就进行扩容判断,扩容的条件为两个
    • 1.HashMap中的元素个数size大于扩容阈值threshold
    • 2.要插入的键值对中key对应的数组索引位置不为null
  • jdk1.8中在插入节点以后才进行扩容判断,扩容的条件为一个:
    • 新增节点后HashMap中的元素个数size大于扩容阈值threshold

变化6:

  • jdk1.7中hash方法比较复杂
  • jdk1.8中对hash方法进行了简化

变化7:

  • jdk1.7中使用数组+链表
  • jdk1.8中使用数组+链表+红黑树

变化8:

  • jdk1.7中容量初始化为2的幂次方,只有一个作用
    • 通过计算数组的索引值
  • jdk1.8中容量初始化为2的幂次方,有3个作用
    • 1.通过计算数组的索引值
    • 2.在扩容时通过 判断要将该节点加入高位链表还是低位链表
    • 3.通过以及这几个运算的巧妙结合,避免了rehash

变化9:

  • jdk1.7中需要进行rehash
  • jdk1.8中不需要进行rehash

5.HashMap实现中对组合模式的应用

见我的另一篇博客:https://blog.csdn.net/qq_34805255/article/details/98480704

6.HashMap中常用方法使用

import java.util.Collection;
import java.util.HashMap;
import java.util.Set;

    public class HashMapDemo {

        public static void main(String[] args) {
            HashMap<String, String> map = new HashMap<String, String>();
            // 键不能重复,值可以重复
            map.put("fei", "张飞");
            map.put("yun", "赵云");
            map.put("yu", "关羽");
            map.put("zhang", "张飞");
            map.put("zhang", "张飞2");// 张飞
            map.put("zhong", "黄忠");
            System.out.println("-------直接输出hashmap:-------");
            System.out.println(map);
            /**
             * 遍历HashMap
             */
            // 1.获取Map中的所有键
            System.out.println("-------foreach获取Map中所有的键:------");
            Set<String> keys = map.keySet();
            for (String key : keys) {
                System.out.print(key+"  ");
            }
            System.out.println();//换行
            // 2.获取Map中所有值
            System.out.println("-------foreach获取Map中所有的值:------");
            Collection<String> values = map.values();
            for (String value : values) {
                System.out.print(value+"  ");
            }
            System.out.println();//换行
            // 3.得到key的值的同时得到key所对应的值
            System.out.println("-------得到key的值的同时得到key所对应的值:-------");
            Set<String> keys2 = map.keySet();
            for (String key : keys2) {
                System.out.print(key + ":" + map.get(key)+"   ");

            }
            /**
             * 另外一种不常用的遍历方式
             */
            // 当我调用put(key,value)方法的时候,首先会把key和value封装到
            // Entry这个静态内部类对象中,把Entry对象再添加到数组中,所以我们想获取
            // map中的所有键值对,我们只要获取数组中的所有Entry对象,接下来
            // 调用Entry对象中的getKey()和getValue()方法就能获取键值对了
            Set<java.util.Map.Entry<String, String>> entrys = map.entrySet();
            for (java.util.Map.Entry<String, String> entry : entrys) {
                System.out.println(entry.getKey() + "--" + entry.getValue());
            }

            /**
             * HashMap其他常用方法
             */
            System.out.println("after map.size():"+map.size());
            System.out.println("after map.isEmpty():"+map.isEmpty());
            System.out.println(map.remove("fei"));
            System.out.println("after map.remove():"+map);
            System.out.println("after map.get(zhong):"+map.get("zhong"));
            System.out.println("after map.containsKey(yu):"+map.containsKey("yu"));
            System.out.println("after containsValue(张飞2):"+map.containsValue("张飞2"));
            System.out.println(map.replace("yun", "赵云2"));
            System.out.println("after map.replace(yun, 赵云2):"+map);
        }

    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值