Java集合(2)—— HashMap源码分析(jdk1.8)

JDK1.8中的HashMap与旧版本相比,最大的区别是,他的底层结构由原先的数组+链表变为数组+链表+红黑树。不难理解,当冲突发生的比较频繁时,用链表解决冲突的一大问题是查询时遍历链表时间开销较大,因此改为红黑树,提高效率。

HashMap字段

要注意的是:

  • 这里的默认初始容量、最大容量都规定了必须为2的幂,原因待会再解释。
  • 在不同情况下,链表和红黑树是相互转化的,具体的临界条件看注释。
/**
     * 默认初始容量(默认是16),注意这个容量必须为2的幂。
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * 最大容量,如果定义的时候用一个更大的数字定义容量,自动变成这个最大容量。
     * 小于等于2的30次方,同样也必须为2的幂。
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 默认的装载因子为0.75
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 在hashmap中加入一个元素后,检查桶中元素是否超过该阈值,超过就转换为红黑树
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     *删除元素时,如果桶中元素小于6,红黑树转化为链表
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 整个HashMap中的元素大于64时,才会进行链表 --> 红黑树,否则如果桶内元素大于8,进行扩容
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
    /**
     * Node数组
     */
    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内元素的个数
     */
    transient int size;

    /**
     * 更新次数
     */
    transient int modCount;


    // 超过threshold就resize,threshold 等于 capacity*装载因子
    int threshold;


    /**
     * 装载因子,指的是 元素数(size)/capacity, capacity为table的长度
     *
     * @serial
     */
    final float loadFactor;

四种构造方法

  • 声明初始容量和装载因子:
    public HashMap(int initialCapacity, float loadFactor)
  • 只声明初始容量:
    public HashMap(int initialCapacity)
  • 缺省的构造函数:public HashMap()
  • 以另一个Map进行初始化:
    public HashMap(Map<? extends K, ? extends V> m)

哈希中的一些技巧

在HashMap的哈希映射中,没有简单粗暴的让 hash=key.hashcode(),而是出于减少冲突的目的,用了一些技巧。

首先,在映射地址的时候,往往我们会直接用 hashcode%tableLength, 做一个取模运算。问题在于,取模操作的开销是比较大的。因此,为了减少开销,要想办法把取模运算转化为计算机开销最小的位运算,比如&、^之类的。在JDK源码中,用到的优化方法就是将hashcode%tableLength转化为 hashCode & (tableLength - 1)

当tableLength为2的幂的时候,hashCode & (tableLength - 1) 相当于hashCode%tableLength,把开销较大的取模运算转化为与运算。取hashcode的低tableLength位,能把所有这些位都考虑到(如果tableLength-1的低tableLength位有0,也就是tableLength不为2的幂的时候,相当于与运算之后的有些位直接置0,降低了离散度),最大化离散程度。

此外,如果tableLength比较小,hashcode便只有低位参与运算,容易造成冲突。因此,hash = (h=hashcode(key)) ^ (h>>16),即hashcode的低16位与高16位进行亦或操作,减少冲突。

新增元素

插入<key, value>流程如下:

  • 如果数组为空,扩容
  • 计算地址i,查看table[i]是否为null,若为null,创建一个新结点放进去,结束。
  • 若上一步的table[i]不为null,看看table[i]的key是否等于要插入的key,如果等于,覆盖旧的value,结束。
  • 若上一步两个key不相等,则判断table[i]的结点是树结点还是链表结点,并按照各自种类的方法插入。(如果是链表结点,插入结束后判断链表长度是否大于8,决定是否需要树形化)
  • 插入完以后,看看需不需要扩容。

扩容

首先,计算出新容量和新的threshold, 然后进行扩容:

最简单粗暴的扩容方法是:定义一个数组,容量为原来的两倍,然后把所有节点重新散列,计算他们的新地址,放到新数组里,这也是JDK1.7里面的做法。但是这样的开销就比较大啦,所以在JDK1.8里有了一些优化,具体如下:

对于桶中的元素,假设旧数组长度为2k, 则该元素在数组中的索引由 hashcode&(2k-1)变为hashcode&(2k+1-1)。如果k为3的话,括号里的项则是由0111变为1111。那么,如果hashcode的第k位(这里是3)为0,则两个运算结果并没有差别;如果hashcode第k位为1,则等于原来的结果加上1000,也就是加上旧数组的长度2k。如何计算第k位是0还是1呢,很简单,跟旧数组长度2k做与运算就行了。

所以在JDK1.8里的优化就是,把一个桶里的所有元素分为两批,一批的hashcode第k位是0,一批是1。按照原来的顺序不变,把他们分成两批,一个放在原来的索引位置,一个放在旧索引+旧表长的位置上。在这里没有判断是否需要树形化,在插入元素的时候会判断。

线程安全性(不安全)

不安全性主要表现在三个方面:

  1. 多个线程进行put时,线程A获取头结点后被挂起,线程B获取同一个头结点进行头插法插入,线程A继续执行,在旧的头结点前添加元素(将新元素的next置为头结点,再将新元素设为table中的node, 覆盖了线程B的插入结果)。

  2. 扩容时:
    (1) 如果多个线程同时进行扩容,每个线程都会生成一个新容量的新数组,最终将其设置为table,但最终的结果都会被最后结束的那个线程所生成的数组覆盖.
    (2) 如果线程A刚刚进入resize就被挂起,另一个线程执行完resize以后,扩容完毕。此时线程A继续执行resize,而此时得到的table是已经扩容以后的,再次扩容就扩了4倍。

  3. jdk1.7中的hashmap在多线程下rehash的时候,可能会产生循环链表,导致死循环。

与Hashtable的区别

  • 继承关系上:Hashtable继承的是比较老的Dictionary类(现在似乎已经不怎么用了),而HashMap继承的是AbstractMap类,两者都实现了Map接口
  • 最大的区别:Hashtable是线程安全的,而HashMap默认线程不安全。在单线程的情况下,用Hashmap即可,效率比较高。在多线程场景下,不能直接用HashMap,如果要用的话,需要做以下声明(或直接用ConcurrentHashMap):
HashMap<E> map = new HahMap<>();
map = Collections.synchronizedMap(map);
  • 一些细节上的变化
    (1) HashMap的key和value都可以为null, 而Hashtable则均不可以。
    (2) HashMap只有containsKey和containsValue,去掉了Hashtable还拥有的contains,避免歧义.
    (3) 遍历的方式不一样,HashMap是用Iterator遍历
    (4) 初始容量、扩容的倍数不一样

  • 底层实现上:HashMap底层由数组+链表+红黑树实现,Hashtable没有使用红黑树。HashMap的一些优化也是Hashtable没有的,比如Hashtable里面没有用位运算去优化一些操作.

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值