Jdk1.8源码分析-HashMap

1 . 数据结构:

  • JDK 1.7 之前 HashMap 是数组+链表 俗称哈希表组成 key的 (h = key.hashCode()) ^ (h >>> 16)
  • JDK 1.8 之后 HashMap 是数组+链表+红黑二叉树 俗称哈希表组成

2. 源码分析

2.1 属性

 //默认容量大小为16. 必须是2的次幂
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    //HashMap 的最大容量, 必须是2的次幂,并且如果构造方法初始化大于 1<<30 ,容量就按照 1<<30 这个值
    static final int MAXIMUM_CAPACITY = 1 << 30;

    //HashMap 默认的负载因子是0.75f,当 HashMap 中元素数量超过 容量*装载因子 时,进行resize()扩容操作
	// 比如默认的是  16*0.75 = 12 ,也就是说当插入第 13 个数据时, 就扩容到 32 
    static final float DEFAULT_LOAD_FACTOR = 0.75f;


    //当hash有冲突的时候,其链表长度达到8时将链表转换为红黑树 (这是JDK1.8对HashMap的优化,以前一直都是链表)
    static final int TREEIFY_THRESHOLD = 8;

    //当扩容的时候小于 6 时将解决 hash 冲突的红黑树转变为链表
    static final int UNTREEIFY_THRESHOLD = 6;

    //当需要将解决 hash 冲突的链表转变为红黑树时,需要判断下此时数组容量,若是由于数组容量太小(小于&emsp;MIN_TREEIFY_CAPACITY&emsp;)
	//导致的 hash 冲突太多,则不进行链表转变为红黑树操作,转为利用&emsp;resize() 函数对&emsp;hashMap 扩容&emsp;
    static final int MIN_TREEIFY_CAPACITY = 64;
	
	 // hashmap的存的东西, 也就是数组中每个元素都是 Node(节点), 如果说,一个hashmap中存放的数没有哈希冲突,那么他就是个数组
    transient Node<K,V>[] table;

     //由&emsp;hashMap 中 Node<K,V>&emsp;节点构成的 set, 也就是说记录hashmap中所有的 数据
    transient Set<Map.Entry<K,V>> entrySet;

   // hashmap当前存储的元素的数量
    transient int size;
	// 
    transient int modCount;
	
	//临界值 当实际大小(容量*填充比 (capacity * load factor))超过临界值时,会进行扩容
    int threshold;

   // 加载因子, 一般没动过这个,用默认的就可以了  0.75
    final float loadFactor;
复制代码
  • 加载因子:如果哈希表中的元素放得太满,就必须进行rehashing(再哈希)。再哈希使哈希表元数增倍,并将原有的对象重新导入新的哈希表元中,而原始的哈希表元被删除。load factor(加载因子)决定何时要对哈希表进行再哈希
  • 为什么要引入红黑树:
    当哈希冲突比较多的时候,因为链表的长度很大 , 链表是不利于查询的,有利于删除和插入,所以引进了红黑树,每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置.
  • 为什么负载因子默认为0.75f ? 能不能变为0.1、0.9、2、3等等呢?
    0.75是平衡了时间和空间等因素; 负载因子越小桶的数量越多,读写的时间复杂度越低(极限情况O(1), 哈希碰撞的可能性越小); 负载因子越大桶的数量越少,读写的时间复杂度越高(极限情况O(n), 哈希碰撞可能性越高)。 0.1,0.9,2,3等都是合法值。

2.2 构造方法

  • 构造方法一 : 默认构造方法的加载因子是load factor(0.75), size 是16, 所有的属性都是默认的
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
复制代码
  • 构造方法二 : 初始化 容量 和 加载因子
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
		// 如果size<0的时候抛异常
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
		// 如果size>MAXIMUM_CAPACITY(1<<30)的时候,强制设为MAXIMUM_CAPACITY(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;
	//方法返回的值是最接近 initialCapacity 的2的幂,若指定初始容量为9,则实际 hashMap 容量为16,因为必须是2的次幂
    this.threshold = tableSizeFor(initialCapacity);
}
复制代码
  • 构造方法三 : 初始化容量,默认的加载因子
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
复制代码
  • 构造方法四 : 以一个hashmap 来创建一个hashmap
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
复制代码

3. HashMap 的节点

是HashMap的内部类,

 static class Node<K,V> implements Map.Entry<K,V> {
		// 保存的hash值
        final int hash;
		// 保存 key 
        final K key;
		// 保存 value
        V value;
		// 指向下一个Node(节点)
        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; }
		
		// key 的 hash 值和 value 的 hash 值的 异或运算符(当两个数字不同时为1,其余为0)
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

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

		// 如果 key 和 value 都相同的话,在返回 true
        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;
        }
    }
复制代码

4. put() 方法

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
复制代码
 /**
 * @param key 的 hash 值
 * @param key 
 * @param value 
 * @param onlyIfAbsent 如果为 true , 不改变存在的值
 * @param evict 如果为 false,则表处于创建模式。
 * @return 以前的值,如果没有则为null
 */
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为null时, 调用resize生成数组table, 并把table赋值给tab
    if ((tab = table) == null || (n = tab.length) == 0)
		// n表示数组的长度
        n = (tab = resize()).length;
		// 如果 此数组中的 (n - 1) & hash 这个位置 没有值 ,也就是没有Node这个节点
    if ((p = tab[i = (n - 1) & hash]) == null)
		// 直接创建一个节点 并且赋值给 tab[i]
        tab[i] = newNode(hash, key, value, null);
		
		//表示hash有冲突,开始处理冲突/
    else {
        Node<K,V> e; K k;
		// 比较已有的hash值 和 要插入 key 和 hashif (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
			//如果hash值相等且key值相等, 则令e指向冲突的头节点
            e = p;
		// 如果 已有的key 和 要插入的key 相等 并且 节点是属于 红黑树, 就按照红黑树的插入方法来
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
		// 按照链表的方式 存进去
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
					//遍历 找到插入位置后,新建节点插入
                    p.next = newNode(hash, key, value, null);
					//若链表上节点超过TREEIFY_THRESHOLD - 1,将链表变为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
		// 如果链表中的key 已经存在
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
			//覆盖key 相同的 value 并return, 即不会执行++modCount
            afterNodeAccess(e);
            return oldValue;
        }
    }
	//修改次数自增
    ++modCount;
	// 如果 size 大于 threshold(阈值),就扩容
    if (++size > threshold)
		//扩容方法
        resize();
    afterNodeInsertion(evict);
    return null;
}
复制代码

过程总结:

  • 判断 数组 是否为空, 如果空 就创建个新的 ,如果不为空 走下面
  • 根据传入的key 判断 hash 值 和 key , 来查 此数组中该位置是否有值(是否冲突)
  • 没冲突直接放到该位置, 有冲突 然后判断是 红黑树 还是 链表 ,然后对应put方法
  • put 之后 记录 修改的次数,然后再判断size 是否大于阈值 (是否扩容)

get() 方法

public V get(Object key) {
        Node<K,V> e;
        // 直接从node里面拿到值
        return (e = getNode(hash(key), key)) == null ? null : e.value;
}
复制代码

resize() 扩容方法机制 和 方法

  • 扩容机制:原来的hash值新增的是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
	// 旧数组的 size 容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
	// 旧的 阈值
    int oldThr = threshold;
	// 新的容量和阈值初始化
    int newCap, newThr = 0;
    if (oldCap > 0) {
		// 如果 容量 大于最大容量 1<<30,阈值就等于 Int 的最大值 即 不再扩容了
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
		// 两个过程
		// 1. 设置新的容量 : newCap(新的容量) = oldCap(旧容量) * 2  扩大为原来的2倍
		// 2. 设置新的阈值 : 如果 newCap(新的容量) 小于 最大值 并且 旧容量 大于 默认的容量值 , 新的阈值设为 旧的阈值的2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
	// 如果旧表的长度的是0,就是说第一次初始化表
	// 如果旧的阈值 大于 0 , 新的容量 = 旧的阈值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
	// 表示 旧表 容量 和 阈值 都等于 0 ,表示 从来没有初始化过
    else {               // zero initial threshold signifies using defaults
	// 赋值个默认值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
	// 如果新的阈值 等于 0 ,按阈值计算公式(size*加载因子)进行计算
    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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
	// 赋值给全局的 table
    table = newTab;
	// 把旧的数组中的值, 按某种规则 迁移 到新的数组中
    if (oldTab != null) {
		//遍历桶数组,并将键值对映射到新的数组中
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
				//置为null, 方便进行GC
                oldTab[j] = null;
                if (e.next == null)
					//说明这个node中没有链表,直接放在新表的e.hash & (newCap - 1)位置
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
					// 如果 此节点是 红黑树(也就是说有冲突), 采用红黑树管理冲突的键值对
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
				// 证明此节点是 链表 的形式解决冲突的
				//新表是旧表的两倍容量,实例上就把单链表拆分为两队,
&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;//e.hash&oldCap为偶数是低队,e.hash&oldCap为奇数的是高队
                    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);
                    if (loTail != null) {
						//低队不为null,放在新表原位置
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
						//高队不为null,放在新表 j + oldCap 的位置
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
复制代码

总结:

在jdk1.7之前采用 数组+单链表 的方式来储存键值对,在jdk1.8之后采用的是数组+单链表+红黑树,hashmap 默认的size 是16,之后每次扩充,容量变为原来的2倍. 最大容量是2的30次幂,默认的加载因子是 0.75,数组(Node节点)是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,为什么要引入红黑树:当哈希冲突过多时,也就是特定索引位置说链表过长时,咱们都知道链表查询难,插入删除比较快,所以用红黑树来优化,也就是说当链表长度大于7的时候,就会转化成红黑树.hashmap 是 不安全的,因为put不是同步的,然后调用真正的addNode也是不同步的,HashMap存在扩容的情况,会同时拿到一个数组,然后进行扩容,所以扩起来的也不一样.

下图为16扩充为32的resize示意图

参考文章

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值