深入理解HashMap

648 阅读9分钟

一, 什么是HashMap

Java中Map是用来存储键值对<key,value>的集合类,也就是哈希表,而HashMap是Map的实现类.

具有存储效率高,查询快的特点,但不是线程同步的,按照哈希表的特点,Map中的key是不能重复的.

image-20210212112919731

二, 实现原理

HashMap采用数组+链表+红黑树实现
每个数组空间采用Node<key,value>节点来保存键值对,在一定的条件下会采用TreeNode<key,value>保存数据.

image-20210212112919731

三, 基本属性介绍

//The default initial capacity - MUST be a power of two
//默认初始容量-必须是2的幂.
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
//The maximum capacity, used if a higher value is implicitly specified by either of the constructors with arguments.MUST be a power of two <= 1<<30.
//哈希数组最大容量,如果构造函数制定了更大的值,则只使用最大值,并且大小只能为2的幂
static final int MAXIMUM_CAPACITY = 1 << 30
//The load factor used when none specified in constructor
//构造函数中未指定时使用的负载系数(默认使用)
static final float DEFAULT_LOAD_FACTOR = 0.75f
//The bin count threshold for using a tree rather than list for a bin.
//Bins are converted to trees when adding an element to a bin with at least this many nodes.
//The value must be greater than 2 and should be at least 8 to mesh with assumptions in tree removal about //conversion back to plain bins upon shrinkage.
//树化阈值: 当一个数组框中的节点数量大于TREEIFY_THRESHOLD时应当采用树来存储,而不是链表
static final int TREEIFY_THRESHOLD = 8
//The bin count threshold for untreeifying a (split) bin during a resize operation.
//Should be less than TREEIFY_THRESHOLD, and at most 6 to mesh with shrinkage detection under removal.
//树节点数量小于6个时,则将树转换为链表
static final int UNTREEIFY_THRESHOLD = 6
//The smallest table capacity for which bins may be treeified.
//树化的最小阈值: 只有哈希表中的节点数量大于64时,才能将链表转换为树
static final int MIN_TREEIFY_CAPACITY = 64
//存储节点的哈希数组
transient Node<K, V>[] table
//哈希表中的节点数量
transient int size
//修改HashMap的次数,用于fail-fast
transient int modCount
//The next size value at which to resize (capacity * load factor)
//下一次需要扩容的界限(扩容阈值)
int threshold
//The load factor for the hash table
//负载系数,主要用来调节哈希表的填充度
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;
    //设置下一次扩容的阈值,这里不是真正的阈值,后面会重新设置
    this.threshold = tableSizeFor(initialCapacity);
}

五, 常用方法介绍

/**
 * 计算传入值的hash值,用来计算存储的位置
 * 将hashCode的高16位与低16位进行异或操作,这样做的原因主要是为了更好的分散hash分布,减少哈希碰撞
 * 如果采用&则计算出来的结果会向0靠近,采用|计算的结果会向1靠近,都不能做到均匀分布
 * 做异或操作的原因: 加大对哈希码低位的随机性
 * String s = "Hello World!";
 * Integer.toBinaryString(s.hashCode())
 * h.hashCode() = 1100 0110 0011 1100 1011 0110 0001 1101
 * h.hashCode() >>> 16 = 0000 0000 0000 0000 1100 0110 0011 1100
 * 1100 0110 0011 1100 1011 0110 0001 1101  ^
 * 0000 0000 0000 0000 1100 0110 0011 1100
 * 1100 0110 0011 1100 1000 0110 0001 1100
 */
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
 * 刚方法主要用来计算哈希表的大小,从上面的属性可知,哈希表的大小始终为2的幂
 * 这样做的原因是:
 * 1. length的二进制表示始终为100...000,length - 1的二进制为1111...111
 * 2. 长度等于2的幂时,hash%length = hash&(length - 1),但后者比前者效率更高
 * 3. 可以保证计算的位置更加的分散,如果长度为奇数,则length - 1为偶数,无论怎么取余或&操作,最终结果都为偶数,会造成一半空间浪费
 */
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;
}

获取节点

image-20210212125544816

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) {
        // 始终检查首节点是否为要找的节点
        if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        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;
}

//查找节点
final TreeNode<K, V> getTreeNode(int h, Object k) {
    return ((parent != null) ? root() : this).find(h, k, null);
}

//获取树的根节点
final TreeNode<K, V> root() {
    for (TreeNode<K, V> r = this, p; ; ) {
        //只有根节点的父节点为空
        if ((p = r.parent) == null)
            return r;
        //向父节点移动
        r = p;
    }
}

//在树中查找
final TreeNode<K, V> find(int h, Object k, Class<?> kc) {
    TreeNode<K, V> p = this;        //复制当前节点
    do {
        int ph, dir;    //当前节点的hash值,方向
        K pk;           //当前节点的key
        TreeNode<K, V> pl = p.left, pr = p.right, q;    //当前节点的左孩子,右孩子,以及q来返回查找到的结果
        //当前节点的hash值大于需要找的节点的hash值,需要向左子树移动
        if ((ph = p.hash) > h)
            p = pl;
        //当前节点的hash值小于需要找的节点的hash值,需要向右子树移动
        else if (ph < h)
            p = pr;
        //当前节点的hash值相同,并且key也相同,直接返回
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        //左子树为空,向右子树移动查找
        else if (pl == null)
            p = pr;
        //右子树为空,向左子树移动判断
        else if (pr == null)
            p = pl;
        //这一步主要通过Comparable对比当前遍历的节点p的key和要查找的key的大小
        else if ((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc, k, pk)) != 0)
            p = (dir < 0) ? pl : pr;        //小于0向左遍历,大于0向右遍历
        //这一步表示无法通过Comparable比较大小,则从右孩子继续递归遍历查找
        else if ((q = pr.find(h, k, kc)) != null)
            return q;
        else
            //从右孩子递归遍历后还没有找到,从左孩子继续查找
            p = pl;
    } while (p != null);
    return null;
}

static int compareComparables(Class<?> kc, Object k, Object x) {
    return (x == null || x.getClass() != kc ? 0 : ((Comparable) k).compareTo(x));
}

插入节点

put

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, 如果哈希表为空,则通过resize()创建.所以,初始化链表的时机为第一次调用put函数时
         */
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        /*
         * 判断是否存在hash冲突:
         * 若不存在,则直接在该数组位置新建节点,直接插入
         * 否则,代表hash冲突,即当前存储位置已存在节点,则依次向下判断
         * a. 当前位置的key与要存入位置的key相同
         * b. 判断需插入的数据结构是否为红黑树或链表
         */
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K, V> e;       //待修改的节点
            K k;
            /*
             * a. 判断table[i]的元素的key是否与需插入的key一样,如果一样直接用新的value覆盖旧的value
             */
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                /*
                 * b. 判断需要插入的是否为红黑树,如果为红黑树则直接在树中插入或更新键值对
                 */
            else if (p instanceof TreeNode)
                e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
                /*
                 * c. 如果是链表,则直接在链表中更新键值对
                 * i.  遍历table[i],判断key是否存在,采用equals对比当前遍历节点的key与待插入数据的key,如果相同则直接用新的value覆盖旧value
                 * ii. 遍历完毕没有上述情况,则直接在链表尾部插入新的节点
                 * tips: 新增节点之后,需要判断链表的长度是否大于8,如果是,需要树化
                 */
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //向链表尾部插入
                        p.next = newNode(hash, key, value, null);
                        //如果当前链表的长度大于8,则需要树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果找到了key相同的节点,则直接退出
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //向后移动节点进行遍历
                    p = e;
                }
            }
            /*
             * 找到需要修改的node则直接用新值代替旧值
             */
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //默认实现为空,为了给LinkedHashMap来实现有序map
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //修改次数+1,用于fail-fast
        ++modCount;
        //节点数量大于阈值,需要扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
}

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) {
        //旧容量大于最大值,直接返回旧的哈希表
        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)
        // 如果旧的阈值大于0,则表示哈希表已经被初始化了,则新的容量等于旧的阈值
        newCap = oldThr;
    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({"all"})
    Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        //将每个bucket中都移动到新的bucket中
        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 {
                    // 处理链表节点
                    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);
                    //原索引放在bucket中
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    //原索引+oldCap放到bucket中
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

五, 与之前版本对比

1. hash值计算方式
//1.7 
static final int hash(int h) {
    h ^= k.hashCode(); 
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
//1.8 
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

从上可以看出1.7 一共做了5次异或操作和4次无符号右移操作,而1.8一共做了一次右移和一次异或,效率比1.8高

2. 存储数据结构不同

1.7 采用数组 + 链表解决哈希冲突,链表采用头插法

1.8 采用数组 + 链表 + 红黑树 解决哈希冲突,链表采用尾插法

并且1.7的数据结构还会造成循环链表

问题产生的条件:

多线程同时进行put时,如果同时调用了resize操作,可能会导致循环链表产生,进而会导致后面get的时候产生死循环

问题产生的原因:

多线程下进行put,原本应该在当前节点的next被其他线程移到新数组的空位置上,而当前节点的next指针指向刚放入的节点,从而造成循环链表.

3. 扩容机制不同

1.7 对原先的每一个节点重新计算插入位置

1.8 只有第一个位置上的节点不需要重新计算插入位置,其他的都为当前位置+旧数组容量