散列表及HashMap简析(JDK1.8)

首先了解位运算的概念:
1、^(亦或运算) ,针对二进制,相同的为0,不同的为1。
2、&(与运算) 针对二进制,只要有一个为0,就为0
3、| 两个位只要有一个为1,那么结果就是1,否则就为0
4、~ 取反
5、原码 原码就是符号位加上数字的二进制表示。
6、反码
一个数如果为正,则它的反码与原码相同;一个数如果为负,则符号位为1,(符号位不变化,其余位数取反)。
换言之 该数的绝对值取反(绝对值取反各位都取反)。
为了简单起见,我们用1个字节来表示一个整数:
+7的反码为:00000111
-7的反码为: 11111000
7、 补码
补码:一个数如果为正,则它的原码、反码、补码相同;一个数如果为负,取到反码然后加1。(反码加1就是补码)为了简单起见,我们用1个字节来表示一个整数:
+7的补码为: 00000111
-7的补码为: 11111001

散列表:
散列表(Hash table,也叫哈希表),是根据关键码值(Key—
value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。散列表的核心思想如下:
首先我们使用散列函数将给定键转化为一个“数组的索引”,理想情况下,不同的key会被转为不同的索引,但在实际应用中会遇到不同的键转为相同的索引的情况,这种情况叫做碰撞。
得到了索引后,就可以像访问数组一样,通过这个索引访问到相应的键值对。

散列表就在时间和空间的使用上找到了一个很好的平衡点。它的一个优势在于我们只需调整散列算法的相应参数而无需对其他部分的代码做任何修改就能够在时间和空间的权衡上做出策略调整。
既然是通过给定的键值key可以找到将要存放的位置,那这个过程的实现又是如何呢,这里是通过散列函数来实现的,散列函数的功能就是把任意给定的key转为[0, M-1]上的整数(M为线性表的最大长度)。
注意:在选取散列函数的原则是,一是要尽量使散列函数计算时间短一些,其次是计算出的地址尽可能在整个散列表的地址空间中均匀分布,即金科嫩的使得每个地址平均被映射到的同义词数量相当。散列函数可以考虑求余。
哈希碰撞的处理方式:
线性探查法大致分为下面几种方式
1、顺序探测法
顺序探测法顾名思义,其实就是借助散列函数求得索引之后查看此索引是否被占用,如果已经被其他元素占用,则依次向后顺延,如果索引仍被占用,则继续顺延直到没有用过或者到达顺序表的末尾为止,如图:

iH36Fs.png
2、二次探测法
和顺序探测法相似,只不过这种方法是当此索引被用过,则原索引加一,若仍被占用,则原索引减一,若还被占用,则原索引加四,重复往返的进行此操作,每次加或减次数平方。具体如下图:

iH8zvV.png
3、双散列法
双散列法,其实就是利用两个散列函数进行操作,该方法是开放地址法中最好的方法之一,它的探查序列是:
hi=(h(key)+ih1(key))%m 0≤i≤m-1 //即di=ih1(key)
即探查序列为:
d=h(key),(d+h1(key))%m,(d+2h1(key))%m,…,等。
该方法使用了两个散列函数h(key)和h1(key),故也称为双散列函数探查法。
4、伪随机数法
和上述方法大致类似,只是索引加的是一个随机数。
链表散列法
拉链法
[iHwW4O.png
利用到了链表,其基本思想是,当散列函数产生冲突时,通过一个单链表把具有相同散列地址的元素链接在一起,如上图所示。但是拉链法有其缺点,拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。

HashMap
HashMap基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变
HashMap 的实例有两个参数影响其性能:“初始容量” 和 “加载因子”。容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。HashMap默认初始容量16,加载因子0.75,也就是说最多能放16*0.75=12个元素,当put第13个时,HashMap将发生rehash,rehash的一系列处理比较影响性能,所以当我们需要向HashMap存放较多元素时,最好指定合适的初始容量和加载因子,否则HashMap默认只能存12个元素,将会发生多次rehash操作
通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
注意:hashCode()与equals的区别
hashCode()方法和equal()方法的作用其实一样,在Java里都是用来对比两个对象是否相等一致,那么equal()既然已经能实现对比的功能了,为什么还要hashCode()呢?
因为重写的equals()里一般比较的比较全面比较复杂,这样效率就比较低,而利用hashCode()进行对比,则只要生成一个hash值进行比较就可以了,效率很高,那么hashCode()既然效率这么高为什么还要equals()呢?
因为hashCode()并不是完全可靠,有时候不同的对象他们生成的hashcode也会一样(生成hash值得公式可能存在的问题),所以hashCode()只能说是大部分时候可靠,并不是绝对可靠,所以我们可以得出:
1.equals()相等的两个对象他们的hashCode()肯定相等,也就是用equals()对比是绝对可靠的。
2.hashCode()相等的两个对象他们的equals()不一定相等,也就是hashCode()不是绝对可靠的。在散列表中,hashCode()相等,即两个键值对的哈希值相等。然而哈希值相等,并不一定能得出键值对相等。补充说一句:“两个不同的键值对,哈希值相等”,也就是哈希冲突。
所有对于需要大量并且快速的对比的话如果都用equal()去做显然效率太低,所以解决方式是,每当需要对比的时候,首先用hashCode()去对比,如果hashCode()不一样,则表示这两个对象肯定不相等(也就是不必再用equals()去再对比了),如果hashCode()相同,此时再对比他们的equals(),如果equals()也相同,则表示这两个对象是真的相同了,这样既能大大提高了效率也保证了对比的绝对正确性!

HashMap源码分析
推荐博客:https://blog.csdn.net/weixin_42340670

    //默认容量,必须是2的幂,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
    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)上的链表数大于这个值时会转成红黑树,put方法的代码里有用到
	static final int TREEIFY_THRESHOLD = 8;

	// 也是阈值同上一个相反,当桶(bucket)上的链表数小于这个值时树转链表
	static final int UNTREEIFY_THRESHOLD = 6;

	//源码注释里说是:树的最小的容量,至少是 4 x TREEIFY_THRESHOLD = 32 然后为了避免(resizing 和 treeification thresholds(树化阈值)) 设置成64
	static final int MIN_TREEIFY_CAPACITY = 64;

	//HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。Node类实现了Map.Entry接口,是HashMap内部的一种数据结构,主要是键值对组成,是一个对象结点,HashMap可以想象成一个table数组,而数组中存放的元素是Node<k,v>对象对应的地址。HashMap根据对Node对象的key值进行hash获取hash地址,然后找到相应的hash地址,如果有发生哈希冲突,则将该对象元素存入链表中,当链表元素个数达到一定要求,则将链表结构转化为红黑树
	    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;//key值不变
        V value;//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);//Objects为jdk1.7之后的新添内容,本类由一些操作对象的静态工具方法构成,这些工具方法包括了非空检查、方法的非空参数检查、比较对象的hashCode、为对象返回一个字符串表示、比较两个对象从j.7开始添加该类。该类是使用final修饰,不能被继承。该类的构造方法被私有化(使用private修饰),不能直接创建对象。该类中所有的方法都是静态方法,可以使用类型直接调用(该类不需要创建对象)
        }

        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;
        }
    }

下面单独对hash()方法解析(重点)

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

为什么要h>>>16:
首先将高16位无符号右移16位与低十六位做异或运算。如果不这样做,而是直接做&运算那么高十六位所代表的部分特征就可能被丢失 将高十六位无符号右移之后与低十六位做异或运算使得高十六位的特征与低十六位的特征进行了混合得到的新的数值中就高位与低位的信息都被保留了 ,而在这里采用异或运算而不采用& ,| 运算的原因是 异或运算能更好的保留各部分的特征,如果采用&运算计算出来的值会向1靠拢,采用|运算计算出来的值会向0靠拢 接下来将得到的值与与0xf做&运算 目的是得到后四位的数值,得到后四位的下标在0~15之间 对应的放在哪个桶里面,当然这里存在扩容的问题,根据实际情况确定桶的个数hashMap的存储机制实际上是数组加链表,在这里数组就代表桶的数量 检索时一次性就可以排除掉n-1个区间
从上面的代码可以看到key的hash值的计算方法。key的hash值高16位不变,低16位与高16位异或作为key的最终hash值。(h >>> 16,表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位不变。)
iHhDnf.png
为什么要这么干呢?
这个与HashMap中table下标的计算有关。

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

因为,table的长度都是2的幂,因此index仅与hash值的低n位有关,hash值的高位都被与操作置为0了。
假设table.length=2^4=16。

iHhsHS.png
由上图可以看到,只有hash值的低4位参与了运算。
这样做很容易产生碰撞。设计者权衡了speed, utility, and quality,将高16位与低16位异或来减少这种影响。设计者考虑到现在的hashCode分布的已经很不错了,而且当发生较大碰撞时也用树形存储降低了冲突。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。
容量必须设置成2的幂的原因:
当容量一定是2^n时,h & (length - 1) == h % length,它俩是等价不等效的,这个等式实际上可以推理出来,2^n转换成二进制就是1+n个0,减1之后就是0+n个1,如16 -> 10000,15 -> 01111,那根据&位运算的规则,都为1(真)时,才为1,那0≤运算后的结果≤15,假设h <= 15,那么运算后的结果就是h本身,h >15,运算后的结果就是最后三位二进制做&运算后的值,最终,就是%运算后的余数,

comparableClassFor 和 compareComparables
当put一个新元素时,如果该元素键的hash值小于当前节点的hash值的时候,就会作为当前节点的左节点;hash值大于当前节点hash值得时候作为当前节点的右节点。那么hash值相同的时候呢?这时还是会先尝试看是否能够通过Comparable进行比较一下两个对象(当前节点的键对象和新元素的键对象),要想看看是否能基于Comparable进行比较的话,首先要看该元素键是否实现了Comparable接口,此时就需要用到comparableClassFor方法来获取该元素键的Class,然后再通过compareComparables方法来比较两个对象的大小。

/**
*通过获得对象所属的类来判断是否继承了Comparable接口
* 如果对象x的类是C,如果C实现了Comparable<C>接口,那么返回C,否则返回null
*/
static Class<?> comparableClassFor(Object x) {
    if (x instanceof Comparable) {
        Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
        if ((c = x.getClass()) == String.class) // 如果x是个字符串对象
            return c; // 返回String.class
        /*
         * 为什么如果x是个字符串就直接返回c了呢 ? 因为String  实现了 Comparable 接口,可参考如下String类的定义
         * public final class String implements java.io.Serializable, Comparable<String>, CharSequence
         */ 
 
        // 如果 c 不是字符串类,获取c直接实现的接口(如果是泛型接口则附带泛型信息)    
        if ((ts = c.getGenericInterfaces()) != null) {
            for (int i = 0; i < ts.length; ++i) { // 遍历接口数组
                // 如果当前接口t是个泛型接口 
                // 如果该泛型接口t的原始类型p 是 Comparable 接口
                // 如果该Comparable接口p只定义了一个泛型参数
                // 如果这一个泛型参数的类型就是c,那么返回c
                if (((t = ts[i]) instanceof ParameterizedType) &&
                    ((p = (ParameterizedType)t).getRawType() ==
                        Comparable.class) &&
                    (as = p.getActualTypeArguments()) != null &&
                    as.length == 1 && as[0] == c) // type arg is c
                    return c;
            }
            // 上面for循环的目的就是为了看看x的class是否 implements  Comparable<x的class>
        }
    }
    return null; // 如果c并没有实现 Comparable<c> 那么返回空
}

/**
* 如果x所属的类是kc,返回k.compareTo(x)的比较结果
* 如果x为空,或者其所属的类不是kc,返回0
*/
@SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
static int compareComparables(Class<?> kc, Object k, Object x) {
    return (x == null || x.getClass() != kc ? 0 :
            ((Comparable)k).compareTo(x));
}

如果两者不具有compare的资格,或者compare之后仍然没有比较出大小。那么就要通过一个决胜局再比一次,这个决胜局就是tieBreakOrder方法。
/**
* 用这个方法来比较两个对象,返回值要么大于0,要么小于0,不会为0
* 也就是说这一步一定能确定要插入的节点要么是树的左节点,要么是右节点,不然就无法继续满足二叉树结构了
* 
* 先比较两个对象的类名,类名是字符串对象,就按字符串的比较规则
* 如果两个对象是同一个类型,那么调用本地方法为两个对象生成hashCode值,再进行比较,hashCode相等的话返回-1
*/
static int tieBreakOrder(Object a, Object b) {
    int d;
    if (a == null || b == null ||
        (d = a.getClass().getName().
            compareTo(b.getClass().getName())) == 0)
        d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
                -1 : 1);
    return d;
}

巧妙地tableSizeFor函数

//返回一个比给定整数大且最接近的2的幂次方整数,如给定10,返回2的4次方16. 
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;
    }

这个算法的原理是什么呢:
我们首先假定cap也就是给定数的的形式为00…01XXXXXXX,(X代表可为0也可为1,X前面的1为从最高位开始第一个为1的那一位)
首先是n |= n >>> 1;也就是n变成n与n右移一位之后异或后的值,即
n:------------>00…01XXXXXXX
n>>>1:------>00…001XXXXXX
新n:---------->00…011XXXXXX
也许到这里你还看不出什么结果,那接着看第二步n |= n >>> 2;也就是n变成n与n右移两位之后异或的值,即
n: ------------>00…011XXXXXX
n>>>2:------>00…00011XXXX
新n:---------> 00…01111XXXX
到这里就不用再解释了吧,这个算法不断的通过把第一个1开始后面的位变成1,本例由00…01XXXXXXX->00…011111111,最后再返回n+1;
举一个具体的例子:
iHXVHA.png
接着看源码

	//存放KV数据的数组。第一次使用的时候被初始化,根据需要可以重新resize。分配的长度总是2的幂
	transient Node<K,V>[] table;
	
    //当被调用entrySet时被赋值。通过keySet()方法可以得到map key的集合,通过values方法可以得到map value的集合。 
    transient Set<Map.Entry<K,V>> entrySet;

    //存放在map中的KV映射的总数
    transient int size;

    //HashMap被结构性修改的次数。(结构性修改是指改变了KV映射数量的操作或者修改了HashMap的内部结构(如 rehash)。这个用于fail-fast。
    transient int modCount;

     //The next size value at which to resize (capacity * load factor).
     //当需要resize时的阈值。即当HashMap中KV映射的数量(即size)超过了threshold就会resize。
    int threshold;

    //装载因子
    final float loadFactor;
     
     //构造函数,指定了装载因子和初始容量
	 public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " + loadFactor);                                   
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

	 public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

	public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

	public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

//putMapEntries(Map<? extends K, ? extends V> m, boolean evict)函数将m的所有元素存入本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);
        }
    }
}	

重要方法putVal()

	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //判断HashMap的Node数组是否为空
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//表为空则进行初始化数组
        if ((p = tab[i = (n - 1) & hash]) == null)//判断HashMap的Node数组的hash位置是否为空
            tab[i] = newNode(hash, key, value, null);//如果为空直接插入一个节点
        else { // 如果不为空
            Node<K,V> e; K k;
             // 当前Node的Key和新插入的Key是否相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;// 直接覆盖
            else if (p instanceof TreeNode)// 当前Node是否为红黑树的TreeNode, 如果是红黑树结点的话,进行红黑树插入
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else { // 当前Node是否为单向链表的Node
                for (int binCount = 0; ; ++binCount) {// 遍历单向链表
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 如果哈希冲突(哈希碰撞)的数量大于等于8,将单向链表转换为红黑树
                        // 当执行treeifyBin(tab, hash);的时候,也不一定必须转换成红黑树
                        // 如果一个Node的单向链表的长度小于64,扩容
                        // 如果一个Node的单向链表的长度大于等于64,才将此单向链表转换成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 当前Node的Key和新插入的Key是否相等
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 判断新插入的Node是否已存在,如果已存在根据onlyIfAbsent是否用新值覆盖旧值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e); // 回调以允许LinkedHashMap后置操作,先暂时不用管
                return oldValue;
            }
        }
        ++modCount;// 更改操作次数
        // 扩容
        if (++size > threshold) // 大于临界值
            // 将数组大小设置为原来的2倍,并将原先的数组中的元素放到新数组中
            // 因为有链表,红黑树之类,因此还要调整他们
            resize();
        afterNodeInsertion(evict); // 回调以允许LinkedHashMap后置操作,先暂时不管
        return null;
    }

getNode()方法

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // table已经初始化,长度大于0,根据hash寻找table中的项也不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 桶中第一项(数组元素)相等
        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;
}

重要方法resize()

final Node<K,V>[] resize() {
    // 当前table保存
    Node<K,V>[] oldTab = table;
    // 保存table大小
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    // 保存当前阈值 
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 之前table大小大于0
    if (oldCap > 0) {
        // 之前table大于最大容量
        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)
        newCap = oldThr;
    // oldCap = 0并且oldThr = 0,使用缺省值(如使用HashMap()构造函数,之后再插入一个元素会调用resize函数,会进入这一步)
    else {           
        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"})
    // 初始化table
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 之前的table已经初始化过
    if (oldTab != null) {
        // 复制元素,重新进行hash
        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;//重新hash
                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;
                    // 将同一桶中的元素根据(e.hash & oldCap)是否为0进行分割,分成两个不同的链表,完成rehash
                    do {
                        next = e.next;
                        //e.hash & oldCap 可以对新增的bit位进行判断看是否为 0 若为0则e.hash & oldCap也为0
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。
在resize前和resize后的元素布局如下
ibFaXq.png
思考:
从putVal源代码中我们可以知道,当插入一个元素的时候size就加1,若size大于threshold的时候,就会进行扩容。假设我们的capacity大小为32,loadFator为0.75,则threshold为24 = 32 * 0.75,此时,插入了25个元素,并且插入的这25个元素都在同一个桶中,桶中的数据结构为红黑树,则还有31个桶是空的,也会进行扩容处理,其实,此时,还有31个桶是空的,好像似乎不需要进行扩容处理,但是是需要扩容处理的,因为此时我们的capacity大小可能不适当。我们前面知道,扩容处理会遍历所有的元素,时间复杂度很高;前面我们还知道,经过一次扩容处理后,元素会更加均匀的分布在各个桶中,会提升访问效率。所以,说尽量避免进行扩容处理,也就意味着,遍历元素所带来的坏处大于元素在桶中均匀分布所带来的好处。
如何避免resize:
这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.75*1000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。
resize()链表复制中的 e.hash & oldCap:
由源码发现使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。

ib7OSg.jpg
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
ibHpT0.jpg
因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:
ibHCkV.jpg
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。
附上hashmap resize讲解的链接 https://blog.csdn.net/login_sonata/article/details/76598675

treeifyBin()方法
把容器里的元素变成树结构。当HashMap的内部元素数组中某个位置上存在多个hash值相同的键值对,这些Node已经形成了一个链表,当该链表的长度大于等于7(TREEIFY_THRESHOLD默认值为8,TREEIFY_THRESHOLD - 1 = 7)的时候,会调用该方法来进行一个特殊处理。

/**
 * tab:元素数组,
 * hash:hash值(要增加的键值对的key的hash值)
 */
final void treeifyBin(Node<K,V>[] tab, int hash) {

    int n, index; Node<K,V> e;
    /*
     * 如果元素数组为空 或者 数组长度小于 树结构化的最小限制
     * MIN_TREEIFY_CAPACITY 默认值64,对于这个值可以理解为:如果元素数组长度小于这个值,没有必要去进行结构转换
     * 当一个数组位置上集中了多个键值对,那是因为这些key的hash值和数组长度取模之后结果相同。(并不是因为这些key的hash值相同)
     * 因为hash值相同的概率不高,所以可以通过扩容的方式,来使得最终这些key的hash值在和新的数组长度取模之后,拆分到多个数组位置上。
     */
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize(); // 扩容,可参见resize方法解析

    // 如果元素数组长度已经大于等于了 MIN_TREEIFY_CAPACITY,那么就有必要进行结构转换了
    // 根据hash值和数组长度进行取模运算后,得到链表的首节点
    else if ((e = tab[index = (n - 1) & hash]) != null) { 
        TreeNode<K,V> hd = null, tl = null; // 定义首、尾节点
        do { 
            TreeNode<K,V> p = replacementTreeNode(e, null); // 将该节点转换为 树节点
            if (tl == null) // 如果尾节点为空,说明还没有根节点
                hd = p; // 首节点(根节点)指向 当前节点
            else { // 尾节点不为空,以下两行是一个双向链表结构
                p.prev = tl; // 当前树节点的 前一个节点指向 尾节点
                tl.next = p; // 尾节点的 后一个节点指向 当前节点
            }
            tl = p; // 把当前节点设为尾节点
        } while ((e = e.next) != null); // 继续遍历链表

        // 到目前为止 也只是把Node对象转换成了TreeNode对象,把单向链表转换成了双向链表

        // 把转换后的双向链表,替换原来位置上的单向链表
        if ((tab[index] = hd) != null)
            hd.treeify(tab); //链表转换成树的函数,关键操作
    }
}

treeify()方法
通过TreeNode对象调用,实现该对象打头的链表转换为树结构。

/**
 * 参数为HashMap的元素数组
 */
final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null; // 定义树的根节点
    for (TreeNode<K,V> x = this, next; x != null; x = next) { // 遍历链表,x指向当前节点、next指向下一个节点
        next = (TreeNode<K,V>)x.next; // 下一个节点
        x.left = x.right = null; // 设置当前节点的左右节点为空
        if (root == null) { // 如果还没有根节点
            x.parent = null; // 当前节点的父节点设为空
            x.red = false; // 当前节点的红色属性设为false(把当前节点设为黑色)
            root = x; // 根节点指向到当前节点
        }
        else { // 如果已经存在根节点了
            K k = x.key; // 取得当前链表节点的key
            int h = x.hash; // 取得当前链表节点的hash值
            Class<?> kc = null; // 定义key所属的Class
            for (TreeNode<K,V> p = root;;) { // 从根节点开始遍历,此遍历没有设置边界,只能从内部跳出
                // GOTO1
                int dir, ph; // dir 标识方向(左右)、ph标识当前树节点的hash值
                K pk = p.key; // 当前树节点的key
                if ((ph = p.hash) > h) // 如果当前树节点hash值 大于 当前链表节点的hash值
                    dir = -1; // 标识当前链表节点会放到当前树节点的左侧
                else if (ph < h)
                    dir = 1; // 右侧
 
                /*
                 * 如果两个节点的key的hash值相等,那么还要通过其他方式再进行比较
                 * 如果当前链表节点的key实现了comparable接口,并且当前树节点和链表节点是相同Class的实例,那么通过comparable的方式再比较两者。
                 * 如果还是相等,最后再通过tieBreakOrder比较一次
                 */
                else if ((kc == null &&
                            (kc = comparableClassFor(k)) == null) ||
                            (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);
 
                TreeNode<K,V> xp = p; // 保存当前树节点
 
                /*
                 * 如果dir 小于等于0 : 当前链表节点一定放置在当前树节点的左侧,但不一定是该树节点的左孩子,也可能是左孩子的右孩子 或者 更深层次的节点。
                 * 如果dir 大于0 : 当前链表节点一定放置在当前树节点的右侧,但不一定是该树节点的右孩子,也可能是右孩子的左孩子 或者 更深层次的节点。
                 * 如果当前树节点不是叶子节点,那么最终会以当前树节点的左孩子或者右孩子 为 起始节点  再从GOTO1 处开始 重新寻找自己(当前链表节点)的位置
                 * 如果当前树节点就是叶子节点,那么根据dir的值,就可以把当前链表节点挂载到当前树节点的左或者右侧了。
                 * 挂载之后,还需要重新把树进行平衡。平衡之后,就可以针对下一个链表节点进行处理了。
                 */
                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;
                }
            }
        }
    }
 
    // 把所有的链表节点都遍历完之后,最终构造出来的树可能经历多个平衡操作,根节点目前到底是链表的哪一个节点是不确定的
    // 因为我们要基于树来做查找,所以就应该把 tab[N] 得到的对象一定根节点对象,而目前只是链表的第一个节点对象,所以要做相应的处理。
    moveRootToFront(tab, root); // 单独解析
}

removeNode()方法

/**
* 方法为final,不可被覆写,子类可以通过实现afterNodeRemoval方法来增加自己的处理逻辑(解析中有描述)
*
* @param hash key的hash值,该值是通过hash(key)获取到的
* @param key 要删除的键值对的key
* @param value 要删除的键值对的value,该值是否作为删除的条件取决于matchValue是否为true
* @param matchValue 如果为true,则当key对应的键值对的值equals(value)为true时才删除;否则不关心value的值
* @param movable 删除后是否移动节点,如果为false,则不移动
* @return 返回被删除的节点对象,如果没有删除任何节点则返回null
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
                            boolean matchValue, boolean movable) {
    Node<K,V>[] tab; Node<K,V> p; int n, index; // 声明节点数组、当前节点、数组长度、索引值
    /*
     * 如果 节点数组tab不为空、数组长度n大于0、根据hash定位到的节点对象p(该节点为 树的根节点 或 链表的首节点)不为空
     * 需要从该节点p向下遍历,找到那个和key匹配的节点对象
     */
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        Node<K,V> node = null, e; K k; V v; // 定义要返回的节点对象,声明一个临时节点变量、键变量、值变量
 
        // 如果当前节点的键和key相等,那么当前节点就是要删除的节点,赋值给node
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
 
        /*
         * 到这一步说明首节点没有匹配上,那么检查下是否有next节点
         * 如果没有next节点,就说明该节点所在位置上没有发生hash碰撞, 就一个节点并且还没匹配上,也就没得删了,最终也就返回null了
         * 如果存在next节点,就说明该数组位置上发生了hash碰撞,此时可能存在一个链表,也可能是一颗红黑树
         */
        else if ((e = p.next) != null) {
            // 如果当前节点是TreeNode类型,说明已经是一个红黑树,那么调用getTreeNode方法从树结构中查找满足条件的节点
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            // 如果不是树节点,那么就是一个链表,只需要从头到尾逐个节点比对即可    
            else {
                do {
                    // 如果e节点的键是否和key相等,e节点就是要删除的节点,赋值给node变量,调出循环
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                            (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
 
                    // 走到这里,说明e也没有匹配上
                    p = e; // 把当前节点p指向e,这一步是让p存储的永远下一次循环里e的父节点,如果下一次e匹配上了,那么p就是node的父节点
                } while ((e = e.next) != null); // 如果e存在下一个节点,那么继续去匹配下一个节点。直到匹配到某个节点跳出 或者 遍历完链表所有节点
            }
        }
 
        /*
         * 如果node不为空,说明根据key匹配到了要删除的节点
         * 如果不需要对比value值  或者  需要对比value值但是value值也相等
         * 那么就可以删除该node节点了
         */
        if (node != null && (!matchValue || (v = node.value) == value ||
                                (value != null && value.equals(v)))) {
            if (node instanceof TreeNode) // 如果该节点是个TreeNode对象,说明此节点存在于红黑树结构中,调用removeTreeNode方法(该方法单独解析)移除该节点
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p) // 如果该节点不是TreeNode对象,node == p 的意思是该node节点就是首节点
                tab[index] = node.next; // 由于删除的是首节点,那么直接将节点数组对应位置指向到第二个节点即可
            else // 如果node节点不是首节点,此时p是node的父节点,由于要删除node,所有只需要把p的下一个节点指向到node的下一个节点即可把node从链表中删除了
                p.next = node.next;
            ++modCount; // HashMap的修改次数递增
            --size; // HashMap的元素个数递减
            afterNodeRemoval(node); // 调用afterNodeRemoval方法,该方法HashMap没有任何实现逻辑,目的是为了让子类根据需要自行覆写
            return node;
        }
    }
    return null;
}

有关红黑树的知识暂不解析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值