HashMap原理深度自析

JDK1.8中HashMap的结构

数组+链表+红黑树

  • 当链表超过8且数组长度(数据总量)超过64才会转为红黑树
  • 将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间
    在这里插入图片描述

对HashMap中的属性进行解析

// hashMap默认容量大小,为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// hashMap最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 阈值,当某个节点的链表长度大于8时,转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当某个节点红黑树节点数量小于 6 时, 回退为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 当hashMap中元素数量大于64时,也会转换为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
// 节点数组
transient Node<K,V>[] table;
// entiry集合,用于迭代遍历
transient Set<Map.Entry<K,V>> entrySet;
// map中元素的个数
transient int size;
// map的修改次数
transient int modCount;
// 实际使用的加载因子,当size > 加载因子*table.size时会进行扩容
final float loadFactor;

思考:

  • 为什么加载因子是0.75f?
 	/* Because TreeNodes are about twice the size of regular nodes, we
     * use them only when bins contain enough nodes to warrant use
     * (see TREEIFY_THRESHOLD). And when they become too small (due to
     * removal or resizing) they are converted back to plain bins.  In
     * usages with well-distributed user hashCodes, tree bins are
     * rarely used.  Ideally, under random hashCodes, the frequency of
     * nodes in bins follows a Poisson distribution
     * (http://en.wikipedia.org/wiki/Poisson_distribution) with a
     * parameter of about 0.5 on average for the default resizing
     * threshold of 0.75, although with a large variance because of
     * resizing granularity. Ignoring variance, the expected
     * occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
     * factorial(k)). The first values are:
     *
     * 0:    0.60653066
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     * more: less than 1 in ten million
     * /

理想情况下,在随机hashCodes下,bin中节点的频率遵循Poisson分布,默认调整大小阈值0.75的平均参数约为,尽管由于调整粒度而差异很大。忽略方差,列表大小k的预期出现次数是(exp(-0.5)* pow(0.5,k)/ * factorial(k))。第一个值是:

0:0.60653066
1:0.30326533
2:0.07581633
3:0.01263606
4:0.00157952
5:0.00015795
6:0.00001316
7:0.00000094
8:0.00000006
其他:不到千万分之一

也就是说,我们单个Entry的链表长度为0,1的概率非常高,而链表长度很大,比8还要大的概率忽略不计了。

  • 加载因子为0.5或者1,会怎么样?能大于1吗?

我们凭借逻辑思考,如果加载因子非常的小,比如0.5,那么我们是不是扩容的频率就会变高,但是hash碰撞的概率会低很多,相应的链表长度就普遍很低,那么我们的查询速度是不是快多了?但是内存消耗确实大了。

那么加载因子很大呢?我们想象一下,如果加载因子很大,我们是不是扩容的条件就变的更加苛刻了,hash碰撞的概率变高,每个链表长度都很长,查询速度变慢,但是由于我们不怎么扩容,内存是节省了不少,毕竟扩容一次就翻一倍。

那么加载因子大于1会怎么样,我们加载因子是10,初始容量是16,当桶数达到160时扩容,平均每个链表长度为10,链表并没有长度限制,所以,加载因子可以大于1,但是我们的HashMap如果查询速度取决于链表的长度,那么HashMap就失去了自身的优势,尽管JDK1.8引入了红黑树,但是这只是补救操作。

如果在实际开发中,内存非常充裕,可以将加载因子调小。如果内存非常吃紧,可以稍微调大一点。

  • 为什么初始容量是16?

我们知道扩容是个耗时的过程,有大量链表操作,16作为一个折中的值,即不会存入极少的内容就扩容,也不会在加入大量数据而扩容太多次。16扩容3次就达到128的长度。

其实还有一个很重要的地方,16是2的4次方,我们在看HashMap的源码时,可以看到初始容量的定义方式如下:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
  • 为什么初始容量是2的多次方比较好?

因为我们计算数据插入位置时的计算公式如下:

if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

当数组大小为16时的演示计算过程

1101 0011 0010 1110 0110 0100 0010 1011 原数
    
0000 0000 0000 0000 0000 0000 0000 1111 15的二进制
    
0000 0000 0000 0000 0000 0000 0000 1011 结果

我们发现,插入位置实际上又原数的最低的4位决定的,每个位置都有插入的可能。

如果容量不为2的多次方会导致有多个位置无法插入数据
例如:容量为15时

1101 0011 0010 1110 0110 0100 0010 1011 原数
    
0000 0000 0000 0000 0000 0000 0000 1110 14的二进制
    
0000 0000 0000 0000 0000 0000 0000 1010 结果

我们可得出,最低位永远为0,那么我们的单数位置上永远插入不了数据。

构造方法

共有三个有参构造和一个无参构造
1、如果使用无参构造方法,则创建一个HashMap集合,并且所有属性都使用默认属性
2、如果使用有参构造方法,那么一定要传入初始化容器大小,而加载因子如果没有传入,则使用默认的
3、有参构造方法的参数中可以传入Map集合,会将传入的map中的数据存入新创建的HashMap中

	// 自定义容器大小和加载因子
	public HashMap(int initialCapacity, float loadFactor) {
		// 如果初始化大小小于0,则抛出异常
        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);
    }

	// 自定义初始化大小,并且使用默认加载因子
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

	// 无参构造,所有属性使用默认的
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }
	// 构造参数中传入一个Map,将原map的数据存入新的HashMap中
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

tableSizeFor方法

首先要理解几个位运算符

  • |=:两个二进制对应位都为0时,结果等于0,否则结果等于1;
  • &=:两个二进制的对应位都为1时,结果为1,否则结果等于0;
  • ^=:两个二进制的对应位相同,结果为0,否则结果为1。
	// 如果传入一个非2的幂次方的数,则会自动转换为比传入数大的最近的一个二次方幂
	static final int tableSizeFor(int cap) {
		// 如果传入的cap是2的n次幂,n-1就能保证计算出来的 n值最大为cap
        int n = cap - 1;
        // 保证n有值部分的前两位皆为1,以下按这个规律递增,并且保证高位为0的部分仍为0
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        // 当传入7的时候,经过计算n=7
        // 当传入9的时候,经过计算n=15
        // 因为返回的是n+1,所以返回结果是8或者16皆为2的n次幂
        // n最大为MAXIMUM_CAPACITY ,1<<<30
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

例子:
在这里插入图片描述

put方法

hashMap插入数据主要是有xx个步骤

  • 1、判断数组是否初始化,如果没有初始化则需要先进行初始化
  • 2、判断计算出来的位置上是否有数据,如果没有则直接插入,如果有,则判断key是否相同,如果key相同则直接替换
  • 3、如果两个值的key不相同,则判断该节点是否为红黑树,如果为红黑树,则调用红黑树的插入方法
  • 4、如果不为红黑树,则当前节点为链表,则对链表进行遍历,如果存在key相同的值则进行替换,如果没有相同的key,则进行链表的插入,然后再判断链表长度是否大于等于8,如果大于等于8则转换为红黑树
  • 5、完成数据的插入之后,再判断数据量是否大于阈值,如果大于阈值则进行扩容操作
 	public V put(K key, V value) {
 		// 调用插入方法前,需要先通过hash(key)计算出key的hash值
        return putVal(hash(key), key, value, false, true);
    }

	static final int hash(Object key) {
        int h;
        // 用key的hashCode值与它的高16位进行异或运算
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    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是否初始化,如果没有初始化,则调用resize()方法先初始化
        if ((tab = table) == null || (n = tab.length) == 0)
        	// 将初始化后的table赋值给tab
            n = (tab = resize()).length;
        // 用hash值与n-1进行位与运算,如果tab上该位置为null,则直接将该node放于数组上的该位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 如果该位置已存在节点,则判断该节点的key值与准备存入的key值是否相等
            // 如果相等,则将该节点的值赋予节点e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果不相等,则判断p是否为红黑树,如果为红黑树,则调用红黑树的插入逻辑
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            	// 如果既不相等,又不为红黑树,则对p进行链表遍历
                for (int binCount = 0; ; ++binCount) {
                	// 如果下一节点为null,则创建一个新的节点,并让当前节点的next指向新节点
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 判断链表长度是否大于等于8,如果大于等于8则转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果某一节点的hash与key和准备插入的数据相等,则跳出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    // 否则将e赋予p,继续遍历下一节点
                    p = e;
                }
            }
            // 如果e不为空,则该key已存在与hashMap中,则进行value的覆盖操作
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        // 修改次数+1
        ++modCount;
        // 如果数据量大于阈值,则进行resize()扩容操作
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }
  • HashMap为什么异或原数右移16位计算哈希值?
	static final int hash(Object key) {
        int h;
        // 用key的hashCode值与它的高16位进行异或运算
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

当key为空时,hash值为0,如果key不为空,则使用key的hashCode高16位与低16位进行异或运算
我们看一下效果

0000 1010 1000 1000 1010 0011 0111 0100 原数

0000 0000 0000 0000 0000 1010 1000 1000 右移16

0000 1010 1000 1000 1010 1001 1111 1100 异或结果

不难看出,我们的高16位并没有发生变化,因为右移16位后,高区都是补0,而1异或0是1,1异或1是0,0异或0还是0
我们再看看hasoMap位置计算方法

if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
1101 0011 0010 1110 0110 0100 0010 1011 原数
    
0000 0000 0000 0000 0000 0000 0000 1111 15的二进制
    
0000 0000 0000 0000 0000 0000 0000 1011 结果

我们根据上述可知,当我们hashMap容量小于1<<<16时,高区的16位会被数组槽位数的二进制码锁屏蔽,如果我们不做位移和异或运算,那么在计算槽位时将丢失高区特征。

当我们两个hash值高区差异很大,而低区差异很小时,如果不做位移和异或运算,那我们计算出来两个值的槽位会很接近,就会加大hash碰撞的概率。

一个健壮的哈希算法应该在hash比较接近的时候,计算出来的结果应该也要天差地别,足够的散列,所以这这个高位右移16位的异或运算也是HashMap将性能做到极致的一种体现。

  • HashMap的hash算法为什么使用异或?

异或运算能更好的保留各部分的特征,如果采用&运算计算出来的值会向0靠拢,采用 | 运算计算出来的值会向1靠拢。

  • 为什么要用&运算?

当容量为16时,只计算最后二进制最后四位的情况下,&和%运算结果是一样的,而&运算时二进制逻辑运算符,是计算机能直接执行的操作符,而%是Java处理整形浮点型所定义的操作符,底层也是这些逻辑运算符的实现,效率的差别可想而知,效率相差大概10倍。

resize方法

resize方法主要作用是初始化或者扩容,如果是初始化的话,直接新建一个node数组返回,如果是扩容的话,在不超过最大容量限制的基础上,扩大两倍,然后将原数组上的node数据进行rehash(要么与原来位置相同,要么在原位置基础上加上旧数组长度)得到在新node数组上的位置。

 final Node<K,V>[] resize() {
 		// 创建一个node数组来存放旧的table
        Node<K,V>[] oldTab = table;
        // 如果旧的table为null,则证明还未初始化
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 赋予旧的阈值
        int oldThr = threshold;
        // 新的容量和阈值设为0
        int newCap, newThr = 0;
        // 如果旧的容量大于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
        }
        // 如果阈值大于0,新的容量就赋予为原来的阈值
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
        	// 如果原容量和阈值都为0的情况下,容量设置为默认初始值16
            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 
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        // 拿到新的容量值之后创建一个新的node数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        // 如果原来的oldTab数组为空,则直接返回新的node数组
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                // 遍历原数组
                if ((e = oldTab[j]) != null) {
                	// 拿到原来的数组上的节点,并将原节点置为null
                    oldTab[j] = null;
                    // 如果拿到的节点没有下一个节点,则当前节点只有一个元素,直接将原来的节点重新进行hash计算,得到在新数组上的位置
                    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;
                            // 因为旧数组长度必是2的n次方,所以e.hash&oldCap结果要么是0,要么就是oldCap
                            // 如果是0的话,那么e.hash&(oldCap-1)与e.hash&(newCap-1)结果是相等的 
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 如果不是0的话,那么e.hash&(oldCap-1)与e.hash&(newCap-1)结果是相差oldCap的
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        // 所以当loTail不为空时,则在新数组的原索引位置上,存放原node
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                         // 当hiTail 不为空时,则在新数组的原索引位置+oldCap位置上,存放原node
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

comparableClassFor

putTreeVal方法

  • 根节点(root 节点)指的是红黑树最上面的那个节点,也就是没有父节点的节点。
	final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                       int h, K k, V v) {
            // kc是key所指向的类
            Class<?> kc = null;
            // 是否进行了搜索
            boolean searched = false;
            // 获取根节点
            TreeNode<K,V> root = (parent != null) ? root() : this;
            // 从根节点开始遍历红黑树
            for (TreeNode<K,V> p = root;;) {
            	// dir是指的遍历的方向,ph是指的节点hash值,pk是指的节点的key值
                int dir, ph; K pk;
                // 如果小于根节点的hash值,则向左遍历,-1代表向左
                if ((ph = p.hash) > h)
                    dir = -1;
                // 如果大于根节点的hash值,则向右进行遍历,1代表向右
                else if (ph < h)
                    dir = 1;
                // 如果hash值相同,则判断key是否相同,如果相同,则直接返回根节点
                else if ((pk = p.key) == k || (k != null && k.equals(pk)))
                    return p;
                // comparableClassFor判断kc是否继承Comparable
                // compareComparables判断两个key的class是否相同,如果相同则将key进行比较
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    // 如果kc不可进行比较,或者两个key不同class
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        // 因为key不可进行比较,则对二叉树进行遍历,先遍历左边,再遍历右边,如果有匹配到node,则将node返回
                        if (((ch = p.left) != null &&
                             (q = ch.find(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.find(h, k, kc)) != null))
                            return q;
                    }
                    // 调用tieBreakOrder方法确定新插入节点在红黑树上的方向
                    dir = tieBreakOrder(k, pk);
                }
				// 如果当前遍历到的节点没有下一节点了
                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                	// 即使是红黑树,也会维持一个链表结构
                	// xpn为链表的下一节点
                    Node<K,V> xpn = xp.next;
                    // 创建一个新的节点
                    TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
                    // 如果dir<0则插入到当前节点的左节点
                    if (dir <= 0)
                        xp.left = x;
                    else
                    // 否则插入到右节点
                        xp.right = x;
                    // xp的下一节点设置为x
                    xp.next = x;
                    // x 的parent节点设置为xp
                    x.parent = x.prev = xp;
                    // 如果之前xp有next节点,则将之前的next节点的parent节点设置为x
                    if (xpn != null)
                        ((TreeNode<K,V>)xpn).prev = x;
                    // 经过平衡之后的红黑树根节点至于头结点
                    moveRootToFront(tab, balanceInsertion(root, x));
                    return null;
                }
            }
        }

get方法

  • get方法首先会判断头节点是否为我们要查询的元素,如果不是,则判断该位置是二叉树还是链表,是链表,则直接进行遍历查找,如果为二叉树,则调用二叉树上查询节点的getNode方法
	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;
        // 判断数组是否为空,并且判断数组中的hash所位于的索引是否存在节点
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 判断头节点的hash与key是否与传入的hash和key相同
            if (first.hash == hash && // always check first node
                ((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) {
		// 如果parent节点为空,则当前设置为root节点,不为空则或获取到根节点
        return ((parent != null) ? root() : this).find(h, k, null);
    }
    final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
            TreeNode<K,V> p = this;
            do {
                int ph, dir; K pk;
                // 获取到当前节点的左右节点
                TreeNode<K,V> pl = p.left, pr = p.right, 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;
                // 当hash相同时,并且左右节点都不为空的情况下,比较class,判断向左还是向右
                else if ((kc != null ||
                          (kc = comparableClassFor(k)) != null) &&
                         (dir = compareComparables(kc, k, pk)) != 0)
                    p = (dir < 0) ? pl : pr;
                // 否则先向右递归查找,如果没找到,则向左进行查找
                else if ((q = pr.find(h, k, kc)) != null)
                    return q;
                else
                    p = pl;
            } while (p != null);
            return null;
        }

comparableClassForf方法

static Class<?> comparableClassFor(Object x) {
    // 1.判断x是否实现了Comparable接口
    if (x instanceof Comparable) {
        Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
        // 2.校验x是否为String类型
        if ((c = x.getClass()) == String.class) // bypass checks
            return c;
        if ((ts = c.getGenericInterfaces()) != null) {
            // 3.遍历x实现的所有接口
            for (int i = 0; i < ts.length; ++i) {
                // 4.如果x实现了Comparable接口,则返回x的Class
                if (((t = ts[i]) instanceof ParameterizedType) &&
                    ((p = (ParameterizedType)t).getRawType() ==
                     Comparable.class) &&
                    (as = p.getActualTypeArguments()) != null &&
                    as.length == 1 && as[0] == c) 
                    return c;
            }
        }
    }
    return null;
}

treeifyBin 方法

  • 用于将链表转化为红黑树的方法
	final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 如果数组为空,或者数组长度小于64,则调用resize方法进行初始化或者扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        // 如果数组上该索引位置不为空
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
            	// 将链表节点转化为红黑树
                TreeNode<K,V> p = replacementTreeNode(e, null);
                // 如果是第一次遍历,将头节点赋值给hd
                if (tl == null)
                    hd = p;
                else {
                	// 这是用来维护红黑树中的链表,方便红黑树转化回链表结构
                    p.prev = tl;
                    tl.next = p;
                }
                // 将p节点赋值给tl,用于在下一次循环中作为上一个节点进行一些链表的关联操作
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

	final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            // 从当前节点向下遍历
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                // 如果根节点为空,则将当前节点设置为根节点,根节点为黑色
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key; // k赋值为x的key值
                    int h = x.hash; // h赋值为x的hash值
                    Class<?> kc = null;
                    // 如果当前节点x不是根节点, 则从根节点开始查找属于该节点的位置
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)
                            dir = -1; // 如果x节点的hash值小于p节点的hash值,则将dir赋值为-1, 代表向p的左边查找
                        else if (ph < h)
                            dir = 1; // 如果x节点的hash值大于p节点的hash值,则将dir赋值为1, 代表向p的右边查找
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            // 如果hash值相等,并且 k没有实现Comparable接口 或者 x节点的key和p节点的key相等,则使用一套特殊的规则来决定插入方向
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        // 如果dir小于0,则判断左节点是否为空,如果大于0,则判断右节点是否为空
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                        	// 如果为空,则可以进行节点上的插入操作
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            // 插入完成之后,进行红黑树的插入平衡(通过左旋、右旋和改变节点颜色来保证当前树符合红黑树的要求)
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);
        }

split方法

/**
 * 扩容后,红黑树的hash分布,只可能存在于两个位置:原索引位置、原索引位置+oldCap
 */
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;	// 拿到调用此方法的节点
    TreeNode<K,V> loHead = null, loTail = null; // 存储索引位置为:“原索引位置”的节点
    TreeNode<K,V> hiHead = null, hiTail = null; // 存储索引位置为:“原索引+oldCap”的节点
    int lc = 0, hc = 0;
    // 1.以调用此方法的节点开始,遍历整个红黑树节点
    for (TreeNode<K,V> e = b, next; e != null; e = next) {	// 从b节点开始遍历
        next = (TreeNode<K,V>)e.next;   // next赋值为e的下个节点
        e.next = null;  // 同时将老表的节点设置为空,以便垃圾收集器回收
        // 2.如果e的hash值与老表的容量进行与运算为0,则扩容后的索引位置跟老表的索引位置一样
        if ((e.hash & bit) == 0) {
            if ((e.prev = loTail) == null)  // 如果loTail为空, 代表该节点为第一个节点
                loHead = e; // 则将loHead赋值为第一个节点
            else
                loTail.next = e;    // 否则将节点添加在loTail后面
            loTail = e; // 并将loTail赋值为新增的节点
            ++lc;   // 统计原索引位置的节点个数
        }
        // 3.如果e的hash值与老表的容量进行与运算为非0,则扩容后的索引位置为:老表的索引位置+oldCap
        else {
            if ((e.prev = hiTail) == null)  // 如果hiHead为空, 代表该节点为第一个节点
                hiHead = e; // 则将hiHead赋值为第一个节点
            else
                hiTail.next = e;    // 否则将节点添加在hiTail后面
            hiTail = e; // 并将hiTail赋值为新增的节点
            ++hc;   // 统计索引位置为原索引+oldCap的节点个数
        }
    }
    // 4.如果原索引位置的节点不为空
    if (loHead != null) {   // 原索引位置的节点不为空
        // 4.1 如果节点个数<=6个则将红黑树转为链表结构
        if (lc <= UNTREEIFY_THRESHOLD)
            tab[index] = loHead.untreeify(map);
        else {
            // 4.2 将原索引位置的节点设置为对应的头节点
            tab[index] = loHead;
            // 4.3 如果hiHead不为空,则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)
            // 已经被改变, 需要重新构建新的红黑树
            if (hiHead != null)
                // 4.4 以loHead为根节点, 构建新的红黑树
                loHead.treeify(tab);
        }
    }
    // 5.如果索引位置为原索引+oldCap的节点不为空
    if (hiHead != null) {   // 索引位置为原索引+oldCap的节点不为空
        // 5.1 如果节点个数<=6个则将红黑树转为链表结构
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            // 5.2 将索引位置为原索引+oldCap的节点设置为对应的头节点
            tab[index + bit] = hiHead;
            // 5.3 loHead不为空则代表原来的红黑树(老表的红黑树由于节点被分到两个位置)
            // 已经被改变, 需要重新构建新的红黑树
            if (loHead != null)
                // 5.4 以hiHead为根节点, 构建新的红黑树
                hiHead.treeify(tab);
        }
    }
}

untreeify方法

/**
 * 将红黑树节点转为链表节点, 当节点<=6个时会被触发
 */
final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null; // hd指向头节点, tl指向尾节点
    // 1.从调用该方法的节点, 即链表的头节点开始遍历, 将所有节点全转为链表节点
    for (Node<K,V> q = this; q != null; q = q.next) {
        // 2.调用replacementNode方法构建链表节点
        Node<K,V> p = map.replacementNode(q, null);
        // 3.如果tl为null, 则代表当前节点为第一个节点, 将hd赋值为该节点
        if (tl == null)
            hd = p;
        // 4.否则, 将尾节点的next属性设置为当前节点p
        else
            tl.next = p;
        tl = p; // 5.每次都将tl节点指向当前节点, 即尾节点
    }
    // 6.返回转换后的链表的头节点
    return hd;
}

HashMap 和 Hashtable 的区别

1、HashMap 允许 key 和 value 为 null,Hashtable 不允许。
2、HashMap 的默认初始容量为 16,Hashtable 为 11。
3、HashMap 的扩容为原来的 2 倍,Hashtable 的扩容为原来的 2 倍加 1。
4、HashMap 是非线程安全的,Hashtable是线程安全的。
5、HashMap 的 hash 值重新计算过,Hashtable 直接使用 hashCode。
6、HashMap 去掉了 Hashtable 中的 contains 方法。
7、HashMap 继承自 AbstractMap 类,Hashtable 继承自 Dictionary 类。

总结
1、HashMap 的底层是个 Node 数组(Node<K,V>[] table),在数组的具体索引位置,如果存在多个节点,则可能是以链表或红黑树的形式存在。
2、增加、删除、查找键值对时,定位到哈希桶数组的位置是很关键的一步,源码中是通过下面3个操作来完成这一步:1)拿到 key 的 hashCode 值;2)将 hashCode 的高位参与运算,重新计算 hash 值;3)将计算出来的 hash 值与 “table.length - 1” 进行 & 运算。
3、HashMap 的默认初始容量(capacity)是 16,capacity 必须为 2 的幂次方;默认负载因子(load factor)是 0.75;实际能存放的节点个数(threshold,即触发扩容的阈值)= capacity * load factor。
4、HashMap 在触发扩容后,阈值会变为原来的 2 倍,并且会对所有节点进行重 hash 分布,重 hash 分布后节点的新分布位置只可能有两个:“原索引位置” 或 “原索引+oldCap位置”。例如 capacity 为16,索引位置 5 的节点扩容后,只可能分布在新表 “索引位置5” 和 “索引位置21(5+16)”。
5、导致 HashMap 扩容后,同一个索引位置的节点重 hash 最多分布在两个位置的根本原因是:1)table的长度始终为 2 的 n 次方;2)索引位置的计算方法为 “(table.length - 1) & hash”。HashMap 扩容是一个比较耗时的操作,定义 HashMap 时尽量给个接近的初始容量值。
6、HashMap 有 threshold 属性和 loadFactor 属性,但是没有 capacity 属性。初始化时,如果传了初始化容量值,该值是存在 threshold 变量,并且 Node 数组是在第一次 put 时才会进行初始化,初始化时会将此时的 threshold 值作为新表的 capacity 值,然后用 capacity 和 loadFactor 计算新表的真正 threshold 值。
7、当同一个索引位置的节点在增加后达到 9 个时,并且此时数组的长度大于等于 64,则会触发链表节点(Node)转红黑树节点(TreeNode),转成红黑树节点后,其实链表的结构还存在,通过 next 属性维持。链表节点转红黑树节点的具体方法为源码中的 treeifyBin 方法。而如果数组长度小于64,则不会触发链表转红黑树,而是会进行扩容。
8、当同一个索引位置的节点在移除后达到 6 个时,并且该索引位置的节点为红黑树节点,会触发红黑树节点转链表节点。红黑树节点转链表节点的具体方法为源码中的 untreeify 方法。
9、HashMap 在 JDK 1.8 之后不再有死循环的问题,JDK 1.8 之前存在死循环的根本原因是在扩容后同一索引位置的节点顺序会反掉。
10、HashMap 是非线程安全的,在并发场景下使用 ConcurrentHashMap 来代替。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值