HashMap1.8源码分析

HashMap JDK1.8

HashMap 是一个散列表,它存储的内容是键值对 (key-value) 形式 ,线程安全。

HashMap 最多只允许一条记录的 key键 为 null,允许多条记录的值为 null。
Hash 中不能存在 重复的 key。

它根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。

HashMap 使用 hash 算法进行数据的存储和查询。 内部使用一个 Entry 表示键值对 key-value。 用 Entry 的数组保存所有键值对, Entry 通过链表的方式链接后续的节点 (1.8 后会根据链表长度决定是否转换成一棵树类似 TreeMap 来节省查询时间) Entry 通过计算 key 的 hash 值来决定映射到具体的哪个数组(也叫 Bucket) 中。

HashMap 非线程安全,即任一时刻可以有多个线程同时写 HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用Collections 的 synchronizedMap 方法使 HashMap 具有线程安全的能力,或者使用 ConcurrentHashMap。

HashMap 是数组 + 链表 + 红黑树(JDK1.8 增加了红黑树部分)实现的。

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    private static final long serialVersionUID = 362498820763181265L;
   
    //HashMap 的默认初始容量为 16,必须为 2 的 n 次方
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;


     //HashMap 的最大容量为 2 的 30 次幂
    static final int MAXIMUM_CAPACITY = 1 << 30;        

    
    // HashMap 的默认负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 链表转成红黑树的阈值。即在哈希表扩容时,当链表的长度(桶中元素个数)超过这个值的时候,进行链表到红黑树的转变
    static final int TREEIFY_THRESHOLD = 8;

   //红黑树转为链表的阈值。即在哈希表扩容时,如果发现链表长度(桶中元素个数)小于 6,则会由红黑树重新退化为链表
    static final int UNTREEIFY_THRESHOLD = 6;

    // HashMap 的最小树形化容量。这个值的意义是:位桶(bin)处的数据要采用红黑树结构进行存储时,整个Table的最小容量(存储方式由链表转成红黑树的容量的最小阈值)
	// 当哈希表中的容量大于这个值时,表中的桶才能进行树形化,否则桶内元素太多时会扩容,而不是树形化
    // 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
    static final int MIN_TREEIFY_CAPACITY = 64;

    // Node 是 HashMap 的一个内部类,实现了 Map.Entry 接口,本质是就是一个映射 (键值对)
    // Basic hash bin node, used for most entries.
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash; // 用来定位数组索引位置
        final K key;
        V value;
        Node<K,V> next; // 链表的下一个node

        Node(int hash, K key, V value, Node<K,V> next) { ... }

        public final K getKey()        { ... }
        public final V getValue()      { ... }
        public final String toString() { ... }
        public final int hashCode() { ... }
        public final V setValue(V newValue) { ... }
        public final boolean equals(Object o) { ... }
    }

    //哈希桶数组,分配的时候,table的长度总是2的幂
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     */
    transient Set<Map.Entry<K,V>> entrySet;

	 //HashMap 中实际存储的 key-value 键值对数量
    transient int size;

    //用来记录 HashMap 内部结构发生变化的次数,主要用于迭代的快速失败机制
    transient int modCount;

    // HashMap 的门限阀值/扩容阈值,所能容纳的 key-value 键值对极限,当size>=threshold时,就会扩容
    // 计算方法:容量capacity * 负载因子load factor    
    int threshold;

    //HashMap 的负载因子
    final float loadFactor;
}

Node[] table 的初始化长度 length(默认值是 16)
loadFactor 为负载因子 (默认值 DEFAULT_LOAD_FACTOR 是 0.75),
threshold 是 HashMap 所能容纳的最大数据量的 Node(键值对) 个数。

threshold = length * loadFactor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。

这里我们需要加载因子 (load_factor),加载因子默认为 0.75,当 HashMap 中存储的元素的数量大于 (容量 × 加载因子),也就是默认大于 16*0.75=12 时,HashMap 会进行扩容的操作。

size 这个字段其实很好理解,就是 HashMap 中实际存在的键值对数量。注意和 table 的长度 length、容纳最大键值对数量 threshold 的区别。而 modCount 字段主要用来记录 HashMap 内部结构发生变化的次数,主要用于迭代的快速失败。强调一点,内部结构发生变化指的是结构发生变化,例如 put 新键值对,但是某个 key 对应的 value 值被覆盖不属于结构变化。

在 HashMap 中,哈希桶数组 table 的长度 length 大小必须为 2 的 n 次方 (一定是合数),这是一种非常规的设计,常规的设计是把桶的大小设计为素数。相对来说素数导致冲突的概率要小于合数,具体证明可以参考 http://blog.csdn.net/liuqiyao_01/article/details/14475159,Hashtable 初始化桶大小为 11,就是桶大小设计为素数的应用(Hashtable 扩容后不能保证还是素数)。HashMap 采用这种非常规设计,主要是为了在取模和扩容时做优化,同时为了减少冲突,HashMap 定位哈希桶索引位置时,也加入了高位参与运算的过程。

这里存在一个问题,即使负载因子和 Hash 算法设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响 HashMap 的性能。于是,在 JDK1.8 版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过 8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高 HashMap 的性能,其中会用到红黑树的插入、删除、查找等算法。

hash()方法

HashMap 的 hash 计算时先计算 hashCode(), 然后进行二次 hash。

// 计算二次Hash
int hash = hash(key.hashCode());

// 通过Hash找数组索引
int i = hash & (tab.length-1);

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

这个方法非常巧妙,它总是通过 h &(table.length -1) 来得到该对象的保存位置,而 HashMap 底层数组的长度总是 2 的 n 次方。

当 length 总是 2 的倍数时,h & (length-1) 将是一个非常巧妙的设计:
假设 h=5,length=16, 那么 h & length - 1 将得到 5;
如果 h=6,length=16, 那么 h & length - 1 将得到 6
如果 h=15,length=16, 那么 h & length - 1 将得到 15;
但是当 h=16 时 , length=16 时,那么 h & length - 1 将得到 0 了;
当 h=17 时 , length=16 时,那么 h & length - 1 将得到 1 了。

这样保证计算得到的索引值总是位于 table 数组的索引之内。

put()方法大致的思路为:

1.table[]是否为空

2.判断table[i]处是否插入过值

3.判断链表长度是否大于8,如果大于就转换为红黑二叉树,并插入树中

4.判断key是否和原有key相同,如果相同就覆盖原有key的value,并返回原有value

5.如果key不相同,就插入一个key,记录结构变化一次

public V put(K key, V value) {
    // 对key的hashCode()做hash
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
		//判断table是否为空,如果是空的就创建一个table,并获取他的长度
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
		//如果计算出来的索引位置之前没有放过数据,就直接放入
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
			//进入这里说明索引位置已经放入过数据了
            Node<K,V> e; K k;
			//判断put的数据和之前的数据是否重复
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))   //key的地址或key的equals()只要有一个相等就认为key重复了,就直接覆盖原来key的value
                e = p;
			//判断是否是红黑树,如果是红黑树就直接插入树中
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
			//如果不是红黑树,就遍历每个节点,判断链表长度是否大于8,如果大于就转换为红黑树
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
					//判断索引每个元素的key是否可要插入的key相同,如果相同就直接覆盖
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
			//如果e不是null,说明没有迭代到最后就跳出了循环,说明链表中有相同的key,因此只需要将value覆盖,并将oldValue返回即可
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
		//说明没有key相同,因此要插入一个key-value,并记录内部结构变化次数
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

resize()方法
扩容 (resize) 就是重新计算容量,向 HashMap 对象里不停的添加元素,而 HashMap 对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然 Java 里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。

由于需要考虑 hash 冲突解决时采用的可能是链表也可能是红黑树的方式,因此 resize 方法相比 JDK7 中复杂了一些。

rehashing 触发的条件:1、超过默认容量 * 加载因子;2、加载因子不靠谱,比如远大于 1。

在 HashMap 进行扩容时,会进行 2 倍扩容,而且会将哈希碰撞处的数据再次分散开来,一部分依照新的 hash 索引值呆在 “原处”,一部分加上偏移量移动到新的地方。

具体步骤为:

首先计算 resize() 后的新的 capacity 和 threshold 值。如果原有的 capacity 大于零则将 capacity 增加一倍,否则设置成默认的 capacity。
创建新的数组,大小是新的 capacity
将旧数组的元素放置到新数组中

final Node<K,V>[] resize() {
    // 将字段引用copy到局部变量表,这样在之后的使用时可以减少getField指令的调用
    Node<K,V>[] oldTab = table;
    // oldCap为原数组的大小或当空时为0
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 如果超过最大容量1>>30,无法再扩充table,只能改变阈值
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 新的数组的大小是旧数组的两倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 当旧的的数组大小大于等于默认大小时,threshold也扩大一倍
            newThr = oldThr << 1;
    }
    else if (oldThr > 0) // initial capacity was placed in 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"})
    // 创建容量为newCap的newTab,并将oldTab中的Node迁移过来,这里需要考虑链表和tree两种情况。
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 将原数组中的数组复制到新数组中
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    // 如果e是该bucket唯一的一个元素,则直接赋值到新数组中
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    // split方法会将树分割为lower 和upper tree两个树,如果子树的节点数小于了UNTREEIFY_THRESHOLD阈值,则将树untreeify,将节点都存放在newTab中。
                    // TreeNode的情况则使用TreeNode中的split方法将这个树分成两个小树
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order 保持顺序
                    // 否则则创建两个链表用来存放要放的数据,hash值&oldCap为0的(即oldCap的1的位置的和hash值的同样的位置都是1,同样是基于capacity是2的次方这一前提)为low链表,反之为high链表, 通过这种方式将旧的数据分到两个链表中再放到各自对应余数的位置
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 按照e.hash值区分放在loTail后还是hiTail后
                        if ((e.hash & oldCap) == 0) {
                            // 运算结果为0的元素,用lo记录并连接成新的链表
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            // 运算结果不为0的数据,用li记录
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 处理完之后放到新数组中
                    if (loTail != null) {
                        loTail.next = null;
                        // lo仍然放在“原处”,这个“原处”是根据新的hash值算出来的
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        // li放在j+oldCap位置
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

扩容

我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值