HashMap源码分析笔记 jdk1.7 & jdk1.8

1.HashMap的结构和底层原理

jdk1.7 数组加链表
jdk 1.8 数据加链表加红黑树

数组里面每个地方都存了Key-Value这样的实例,在Java7里面这样的键值对叫Entry,在Java8中叫Node
Tip:因为jdk1.7只有数组加链表,用Entry很合适,jdk1.8有了红黑树,形容树还是节点Node比较合适

每一个节点会保存自身的hash、key、value、以及下个节点,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;
        }
....

2. 为什么需要链表

我们都知道数组长度是有限的,在有限的长度里面我们使用哈希,哈希本身就存在概率性,就是不同的值去hash有一定的概率会一样,这就是Hash冲突,为了解决Hash冲突,用链表或者红黑树去存储Hash值一样的value。


3.Entry节点在插入链表时是怎么插入的

java8之前是头插法,就是说新来的值会取代原有的值,原有的值就顺推到链表中去,就像上面的例子一样,因为写这个代码的作者认为后来的值被查找的可能性更大一点,提升查找的效率。

但是,在java8之后,都是所用尾部插入了。


4. 为什么会采用尾部插入呢?

1.HashMap头插扩容死循环问题
2.jdk1.8引入红黑树之后,反正每次插入元素都要判断这条链有没有超过长度8,在判断长度的时候顺便就插到尾部了
3.还有就是在jdk1.7的时候,就会遍历链表了,在比较有没有相同key的时候。所以不管怎样都会遍历链表的,而且尾插还可以避免死循环,所以就用尾插了

HashMap头插扩容死循环问题:
参考链接: 详解跳转链接:https://coolshell.cn/articles/9606.html

jdk1.7因为头插。同一个槽位的元素会倒置,jdk1.8转变之后那些链表的顺序也不会变,所以头插有循环,尾插没有循环


5.HashMap的初始容量为什么为16

大部分说法,16是一个经验值


6.HashMap的容量为什么是2的幂次方

jdk1.7 indexFor源码

static int indexFor(int h, int length) {
    return h & (length-1);
}

只有容量为2的幂次方,才可以用&运算取模,得到最终的数组的下标。用&运算快。还有在jdk1.8里面,只有容量为2的幂时候,扩容才可以使用那个规律(下面有讲到)。(我觉得就是因为计算机是二进制的,直接使用二进制效率会快很多,所以有关于计算都想往二进制靠)

在jdk1.7中hash的时候为什么要异或,异或之后为什么又要右移

jdk1.7中hash源码:

    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h ^= k.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

异或的原因(个人看法):如果选用&的话,只要当前为是0,高位不管是1还是0,都得是0,高位对结果没有影响。同样,如果是|的话,只要当前是1,结果都是1,和高位的值没有关系。只有^,不管当前是1还是0,高位都能对结果产生影响

右移的原因:indexFor函数取余的时候,高位没有参与运算,也就是尽管高位不同,hash值也可能相同,hash碰撞的概率太大了,右移之后,高位就可以影响我们index的结果了,减小了Hash冲突,让hash值的分布更均匀了

顺便附上jdk1.8hash源码:

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

-------------以下主要是jdk1.7源码分析----------------

1.比较重要的常用变量
	//默认的初始容量,必须是2的n次幂,16也许是个经验值
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
	
	/*
	最大容量为1<<30,超过不会再扩容。为什么是2^30,因为int的范围小于等于2^31-1.
	且要求容量是2的幂次,所以最多只能左移30位
	*/
    static final int MAXIMUM_CAPACITY = 1 << 30;

	//默认加载因子,容量*加载因子得到需要扩容的阈值threshold
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

	//一个空数组,用来比较table是不是为空的
    static final Entry<?,?>[] EMPTY_TABLE = {};
	
	//用来存放Entry的数组
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

	//map中实际键值对个数
    transient int size;
    
	//数组扩容阈值,达到这个值才会扩容
    int threshold;

	//加载因子,一般用默认加载因子
    final float loadFactor;
	
	/*
	每次结构改变时,都会自增,fail-fast机制,这是一种错误检测机制。
	当迭代集合的时候,如果结构发生改变,则会发生 fail-fast,抛出异常。
	*/
    transient int modCount;
 
 	//替代哈希使用的默认扩容阀值(什么东西??***)
    static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;

2.jdk1.7中的构造函数们

HashMap一共有四种构造方法

  1. public HashMap(int initialCapacity, float loadFactor),其他三种构造方法都是调用的这个方法
  2. public HashMap(int initialCapacity)
  3. public HashMap()
  4. public HashMap(Map<? extends K, ? extends V> m)

第一种构造方法:(指定了初始容量和加载因子)

Tip:看完这个构造方法,你会发现它并没有根据传入的initialCapacity去新建一个Entry数组,此时的哈希表依然是个空表。HashMap在构造时不会新建Entry数组,而是在put操作的时候先检查当前hash表是不是个空表,如果是,就调用inflateTable方法进行初始化。inflateTable方法主要就是把初始容量变成2的幂次方,给数组开辟空间,重新设置阈值,还有初始化hashseed,下面put具体讲了

public HashMap(int initialCapacity, float loadFactor) {
		//如果初始容量<0就抛出异常
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
         
//如果初始容量大于最大容量(1<<30),使用最大容量作为初始容量                                  
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
//如果负载率小于等于0或负载率不是浮点数,则抛出异常
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
		//设置加载因子的值
        this.loadFactor = loadFactor;
        //设置阈值为初始容量
        /*
        初始阈值为初始容量,与JDK1.8中不同,JDK1.8中调用了
        tableSizeFor(initialCapacity)得到大于等于初始容量的一个
        最小的2的指数级别数,比如初始容量为12,那么threshold为16,;
        如果初始容量为5,那么初始容量为8
        */
        threshold = initialCapacity;
		//空实现,交由子类实现,比如LinkedHashMap
        init();
    }

第二种构造方法:(指定了初始容量)

//没有指定加载因子,所以是默认的加载因子0.75f
 public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

第三种构造方法:(空的,什么都没指定)

//默认初始容量1<<4,默认加载因子0.75f
public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

第四种构造方法:(使用与指定Map相同的映射,来构造一个新的HashMap。创建该HashMap时,使用默认的装载因子(0.75)和足以容纳指定Map中的映射的初始容量。)

public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        inflateTable(threshold);

        putAllForCreate(m);
    }

应用场景大概是这样的:

    public static void main(String[] args)
    {
        Map<String,Integer>map=new HashMap<>();
        map.put(null,2);
        map.put("zs",4);
        Map<String,Integer>mo=new HashMap<>(map);
        System.out.println(mo);
    }

打印结果:
在这里插入图片描述


3.jdk1.7中的put方法详解
public V put(K key, V value) {
	//transient Node<K,V>[] table; table是一个Node类型的数组
	//1.判断table这个数组是不是一个空的数组,如果是就初始化,详情见下文
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    //2.对key为空进行处理,如果key为空,就putForNullKey(value),详情见下文
    if (key == null)
        return putForNullKey(value);
    //3.获得hash值
    int hash = hash(key);
    //4.进行取余运算,获得index数组下标
    int i = indexFor(hash, table.length);
    //5.遍历链表,在遇到相同的key的时候,用新值覆盖掉旧value,并把旧的value返回
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //这里把hash值写在前面是有小技巧的,见下文
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value; //记录下oldValue
            e.value = value; //用新值覆盖oldValue
            e.recordAccess(this); //在HashMap里面没用,在LinkedHashMap才有用
            return oldValue; //返回oldValue
        }
    }
	// 6.
    modCount++;
    //7. 添加一个节点,详情见下文
    addEntry(hash, key, value, i);
    return null; //还是返回的oldValue
}

1.inflateTable(threshold)方法源码:

初始化操作:

  1. 初始化容量为大于等于的最接近的2的幂次
  2. 初始化阈值
  3. 初始化 table
  4. 初始化 hashseed
    private void inflateTable(int toSize) {
    	//capacity一定是2的次幂,得到大于等于toSize的2的幂,比如5会得到8,9会得到16
        int capacity = roundUpToPowerOf2(toSize);
/*
此处为threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1
的最小值,capaticy一定不会超过MAXIMUM_CAPACITY,
除非loadFactor大于1
我的小问题:为什么这里是最大容量加1啊,最大容量不好吗
*/
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

roundUpToPowerOf2(toSize)方法:

//这个方法是将你希望的数组长度输入,经过计算返回一个2的幂次的数组长度
/*
	1. 如果希望的数组长度达到了最大容量,就返回最大容量作为数组的长度
	2. 否则判断希望的数组长度是否大于1,如果大于1,就返回最接近且大于等于number的2次幂
	3. 如果希望的数组长度小于等于1,就返回1
*/

/*
* 把1排除在外,是因为他(number - 1) << 1) 这个运算会得到0,1是个特判
* number-1是因为直接number<<1得到是大于number的2次幂,
 如果number是2的次幂,比如number=4,就会得到8,我们想要的是4,所以减掉1个
 得到3,再得到大于3的2次幂就刚好是4了
*/
    private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

//这个方法得到的是i的二进制表示里面,最高位的1。比如5(101),就会得到100
/*
程序步骤:
先把i变成全1,比如5(101),就先变成 (111),然后把111无符号右移,变成011,
再用111-011就可以得到100了
解释:
i |= (i >>  1)一直到i |= (i >> 16);就是把这个数变成全1的过程
如果我们用普通方法去把一个数变成全1,是不是只需要将i一直右移直到为0,
就可以把i的每一位和最高位的那个1或一遍。
这个方法只是在普通做法优化了一下。假设x位是最高位,第一次x|=x>>1,这样第x位
和x-1位就都是1了,有两位为1,所以右移两位可以让x-2,x-3变成1,
现在从x,x-1,x-2,x-3,这四位都是1,那么就一起移动这四位,就可以让前8位为1,
前8位再一起移,可以让前16位为1,前16位再一起移,就32位了,
int最多32位,就结束了。

要得到最高位的1,只需要减去除最高位的所有1,所以无符号右移了一个,让他变成011111这样的,相减,就可以得到最高位的1了
*/
    public static int highestOneBit(int i) {
        // HD, Figure 3-1
        i |= (i >>  1);
        i |= (i >>  2);
        i |= (i >>  4);
        i |= (i >>  8);
        i |= (i >> 16);
        return i - (i >>> 1);
    }

initHashSeedAsNeeded方法:遗留问题
hashSeed的初始值在定义的时候就赋值为0了,这个函数是给hashSeed重新赋值
只找到这一篇博客:https://blog.youkuaiyun.com/qq_30447037/article/details/78985216

博客说hashSeed最后的值还是为0,感觉不可能的吧。。为0的话要这个函数干嘛

   final boolean initHashSeedAsNeeded(int capacity) {
        boolean currentAltHashing = hashSeed != 0;
        boolean useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean switching = currentAltHashing ^ useAltHashing;
        if (switching) {
            hashSeed = useAltHashing
                ? sun.misc.Hashing.randomHashSeed(this)
                : 0;
        }
        return switching;
    }

2.判断key=null时候的方法putForNullKey(value)
Tip:因为HashMap里面key是不能重复的,所以null作为key,永远只有一个null。根据如下代码,可知null一直存在数组的第0个位置的

private V putForNullKey(V value) {
	//如果之前有过key=null的,就覆盖,并返回oldValue
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            //在HashMap里面没用,在LinkedHashMap才有用
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}

3.获得hash值
int hash = hash(key);
前面有讲过这个hash
Tip:这里用到了hashSeed

   final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        h ^= k.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

4.进行取余运算,获得index数组下标
int i = indexFor(hash, table.length);

因为length始终是2的幂,本来这句话应该是h%length,最后的结果范围是0~length-1,2^k-1有什么特点,比如2 ^ 3-1,就是(0111),后面全是1。&的特点就是同为1才取1,所以,只要h为1,答案对应就是1,就达到取模的效果了

    static int indexFor(int h, int length) {
        return h & (length-1);
    }

5.遍历链表,在遇到相同的key的时候,用新值覆盖掉旧value,并把旧的value返回。
Tip:
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
把hash判断写在前面的原因:
hash是int类型的,好比较,不像字符串需要一个个对比,很费时间
hash值不等,那他们一定不相等(hash值相等,也可能不等,所以有后面的判断)


6.modCount++;
详解:https://blog.youkuaiyun.com/weixin_39800144/article/details/80613738


7.HashMap 在jdk1.7中是先扩容,再添加的元素。jdk1.8反着来的,先添加元素,再扩容

void addEntry(int hash, K key, V value, int bucketIndex) {
	/*数组的大小大于了阈值且数组上是没有值,才会进行扩容。
	为什么数组上index位置没有值才会扩容呢?
	答:如果有值的话直接加在链表头就好了,占用的是数组的同一个位置,
	只有为null,才会占用一个新的位置,才需要扩容
	为什么要扩容呢?
	答:如果不扩容的话,链表的长度会很长,查找效率会很低
	*/
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length); //见下文
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length)
    }
 
    createEntry(hash, key, value, 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++;
}

resize方法相关:
Tip:在把旧数组元素转移到新数组的时候,hashcode值不需要重新计算,但是数组下标的位置index需要重新计算。因为index的计算方式是h & (length-1),和数组的长度是息息相关的

其实,扩容前的数组下标和扩容后的数组下标是有规律的,所以jdk1.8就是运用的这个规律进行的新旧数组的转移。

规律:
原文链接:https://www.jianshu.com/p/bdfd5f98cc31

由于扩容数组的长度是 2 倍关系,所以对于假设初始 tableSize = 4 要扩容到 8 来说就是 0100 到 1000 的变化(左移一位就是 2 倍),在扩容中只用判断原来的 hash 值与左移动的一位(newtable 的值)按位与操作是 0 或 1 就行,0 的话索引就不变,1 的话索引变成原索引加上扩容前数组
在这里插入图片描述

//扩容机制:
void resize(int newCapacity) {
     // 保存旧的数组
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
     // 判断数组的长度是不是已经达到了最大值
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;//修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
            return;
        }
     // 创建一个新的数组
        Entry[] newTable = new Entry[newCapacity];
     // 将旧数组的内容转换到新的数组中
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
     // 计算新数组的扩容阀值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }


//transfer方法,将旧数组中的内容转移到新的数组
//jdk1.7同一个槽位的元素会倒置,jdk1.8转变之后那些链表的顺序也不会变
	/**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
     // 遍历旧数组得到每一个key再根据新数组的长度重新计算下标存进去,如果是一个链表,则链表中的每个键值对也都要重新hash计算索引
        for (Entry<K,V> e : table) {
      // 如果此slot上存在元素,则进行遍历,直到e==null,退出循环
            while(null != e) {
                Entry<K,V> next = e.next;
         // 当前元素总是直接放在数组下标的slot上,而不是放在链表的最后,所以
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
         // 把原来slot上的元素作为当前元素的下一个
                e.next = newTable[i];
         // 新迁移过来的节点直接放置在slot位置上
                newTable[i] = e;
                e = next;
            }
        }
    }
 

jdk 1.7中get方法详解
public V get(Object key) {
    // 如果key为null,直接去table[0]处去检索即可
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key); // 根据key去获取Entry数组

        return null == entry ? null : entry.getValue();
    }
    

getForNullKey()方法:

    private V getForNullKey() {
        if (size == 0) {
            return null;
        }
        //直接在table第0个位置找对应的value
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
    }

getEntry(key)方法:

    
final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
     // 根据key的hashCode重新计算hash值
        int hash = (key == null) ? 0 : hash(key);
     // 获取查找的key所在数组中的索引,然后遍历链表,通过equals方法对比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;
        }
        return null;
}

entry.getValue()方法:

 public final V getValue() {
     return value;
}

---------------------以下主要是jdk 1.8------------------

jdk1.8主要就是加了个红黑树

红黑树查询快,插入慢。
链表插入快,查询慢。

1.jdk 1.8为什么选择红黑树而不是其他数据结构

引入RB-Tree是功能、性能、空间开销的折中结果。(具体不知道)


2.什么时候树化,什么时候把树改成链表

当链表长度达到8时树化,为6时树转成链表


3.为什么是6的时候把树变成链表呢

因为树化是需要时间的,6和8之间有个中间值,可以避免树和链表频繁的转换


4.为什么有了红黑树还要保留链表呢?

(不知道对不对)
红黑树查询快,插入慢。
链表插入快,查询慢。


5.Node
//Node是单向链表,它实现了Map.Entry接口
static class Node<k,v> implements Map.Entry<k,v> {
    final int hash;
    final K key;
    V value;
    Node<k,v> next;
    //构造函数Hash值 键 值 下一个节点
    Node(int hash, K key, V value, Node<k,v> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
....

6.TreeNode,红黑树节点
   static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        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);
        }


7.比较重要的变量
    //下面三个变量是比jdk1.7多出来的

	//树化的门槛
    static final int TREEIFY_THRESHOLD = 8;

	//桶中节点等于这个数,树转变成链表
    static final int UNTREEIFY_THRESHOLD = 6;

	//树化的另一个判断条件,数组容量大于等于这个数且达到树化的门槛,才进行树化
    static final int MIN_TREEIFY_CAPACITY = 64;


8.jdk1.8中的构造方法们

依然是四种

  1. public HashMap(int initialCapacity, float loadFactor),其他三种构造方法都是调用的这个方法
  2. public HashMap(int initialCapacity)
  3. public HashMap()
  4. public HashMap(Map<? extends K, ? extends V> m)

第一种:(其他三种构造方法和jdk1.7一样

    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;

		//jdk1.7和jdk1.8就这句话不同
		//jdk1.7 threshold = initialCapacity;
		//jdk1.7只是让阈值等于初始容量,然后put的时候去处理的
		//jdk1.8在构造的时候把阈值设置成最接近初始容量的那个2的幂次
        this.threshold = tableSizeFor(initialCapacity);
        
    }

tableSizeFor方法:
相当于jdk1.7里面inflateTable(threshold)中的roundUpToPowerOf2(toSize)中的highestOneBit(int i)方法,不过jdk1.7里面是>>,jdk1.8里面是>>>(无符号右移),
前面那部分都差不多。就最后一句话,jdk1.7是先进行(cap-1)<<1的,jdk1.8这个相当于算完了之后再处理,比如5(101),算完之后是全111,再加1就得到1000了

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

9.jdk1.8中的put方法
 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
 }

jdk1.8中的hash方法:
Tip:相比较jdk 1.7的hash方法简化了很多,1.7之所以那么复杂是为了提高哈希的散列性。提高散列性的主要原因就是为了提高hashMap的查询效率,1.8之后加了红黑树,红黑树查询效率高,所以可以稍微简化一下。

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

putVal(hash(key), key, value, false, true)方法:
这部分参考:https://blog.youkuaiyun.com/weixin_42493179/article/details/88650348

图解:
在这里插入图片描述
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //tab是那个数组,n是数组长度,i是数组下标,p用来保存当前节点,尾插要用
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //判断数组是不是为空,为空进行初始化。jdk1.8里面扩容和初始化都写在resize函数里面的
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //(n-1)&hash 和jdk1.7里面的indexFor是一样的,在取模求数组下标
        /*如果value值为null,说明table[i]还没有被占用过,
        那就直接创建一个新节点放到table[i]的位置。和jdk1.7的区别,
        这里是先添加的元素,后面再进行扩容的*/
        if ((p = tab[i = (n - 1) & hash]) == null)
        //newNode里面直接调用了构造方法
            tab[i] = newNode(hash, key, value, null); 
        else {
        	//e用来,k用来保存key
            Node<K,V> e; K k;
            //判断如果有相同的key就进行覆盖,和jdk1.7一样的。然后最下面还是返回了oldValue的
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //判断当前节点是不是树节点,因为有红黑树和链表两种,插入的方法不一样
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //不是树节点,就是链表
            else {
            	//binCount就是在记录链表的长度,为8要树化那些的
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) { //判断是不是链表的尾节点
                 //如果是尾节点,说明前面也没有遇到key相同的,直接插入就好了
                        p.next = newNode(hash, key, value, null);
                  //TREEIFY_THRESHOLD=8,链表长度大于8树化,暂时减1,因为这里还没++size
                        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;
                }
            }
            //存在重复的key,返回oldValue
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e); 
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize(); //扩容
        afterNodeInsertion(evict);
        return null; //oldValue为空
    }

Tip:阈值>=8的时候也不一定会树化,数组长度小于 MIN_TREEIFY_CAPACITY的时候会扩容。也就是说:链表长度>=8 且 数组容量>=64才会树化

 final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //MIN_TREEIFY_CAPACITY=64
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 
            resize();
......

resize方法:
①.在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;

②.每次扩展的时候,都是扩展2倍;

③.扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。前面讲的那个规律

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;//首次初始化后table为Null
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;//默认构造器的情况下为0
        int newCap, newThr = 0;
        if (oldCap > 0) {//table扩容过
             //当前table容量大于最大值得时候返回当前table
             if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
            //table的容量乘以2,threshold的值也乘以2           
            newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
        //使用带有初始容量的构造器时,table容量为初始化得到的threshold
        newCap = oldThr;
        else {  //默认构造器下进行扩容  
             // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        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) {
                HashMap.Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    // help gc
                    oldTab[j] = null;
                    if (e.next == null)
                        // 当前index没有发生hash冲突,直接对2取模,即移位运算hash &(2^n -1)
                        // 扩容都是按照2的幂次方扩容,因此newCap = 2^n
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof HashMap.TreeNode)
                        // 当前index对应的节点为红黑树,这里篇幅比较长且需要了解其数据结构跟算法,因此不进行详解,当树的高度小于等于UNTREEIFY_THRESHOLD则转成链表
                        ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        // 把当前index对应的链表分成两个链表,减少扩容的迁移量
                        HashMap.Node<K,V> loHead = null, loTail = null;
                        HashMap.Node<K,V> hiHead = null, hiTail = null;
                        HashMap.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);
                        if (loTail != null) {
                            // help gc
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            // help gc
                            hiTail.next = null;
                            // 扩容长度为当前index位置+旧的容量
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

10.jdk1.8 get方法

Tip :
HashMap同样并没有直接提供getNode接口给用户调用,而是提供的get方法,而get方法就是通过getNode来取得元素的。

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

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;
}
总结:jdk1.7和jdk1.8的区别
  1. 最重要的一点是底层结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构;

  2. 构造方法里,给threshold赋的值不一样,jdk1.7是初始容量,jdk1.8是tableSizeFor(initialCapacity)

  3. put方法中,jdk1.7中当哈希表为空时,会先调用inflateTable()初始化一个数组;而1.8则是直接调用resize()扩容,1.8初始化和扩容都是resize方法;

  4. 插入键值对的put方法的区别,1.8中会将节点插入到链表尾部,而1.7中是采用头插;

  5. jdk1.7中的hash函数对哈希值的计算直接使用key的hashCode值,而1.8中则是采用key的hashCode异或上key的hashCode进行无符号右移16位的结果,避免了只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,使元素分布更均匀;

  6. 扩容时1.8会保持原链表的顺序,而1.7会颠倒链表的顺序;而1.8HashMap初始化后首次插入数据时,先发生resize扩容再插入数据,之后每当插入的数据个数达到threshold时就会发生resize,此时是先插入数据再resize。1.7则是在元素插入前;

  7. jdk1.8扩容时transfer方法有那个规律(前面讲的),jdk1.7还要每次进行重新计算index,用 h&(length-1)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值