jdk1.7与1.8HashMap区别

文章目录

  • JDK1.7中HashMap的put()方法全过程。
  • JDK1.8中HashMap的put()方法全过程。
    • 源码及注释
    • 步骤总结
  • JDK1.8有那些变化。
    • JDK1.7中的HashMap
    • JDK1.8中的HashMap
    • 异同
      • 共同点
      • 不同点
      • 建议
  • JDK1.7当中HashMap中线程不安全问题有那些?原因分别是什么?
  • JDK1.8之后如何链地址法,链表长度是多少的时候会转换成红黑树。
  • JDK1.8节点个数是多少的时候,红黑树会退回链表。
  • 为什么会选择8作为链表转红黑树的阈值。
  • HashMap与HashTable有什么区别?
    • HashTable
    • HashMap

JDK1.7中HashMap的put()方法全过程。

jdk1.7与1.8HashMap区别_第1张图片

JDK1.8中HashMap的put()方法全过程。

jdk1.7与1.8HashMap区别_第2张图片

源码及注释

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}


/**
 * Implements Map.put and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node[] tab; 
    Node p; 
    int n, i;

    //步骤①:如果Table为空,初始化一个Table
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

    //步骤②:如果该bucket位置没值,则直接存储到该bucket位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node e; 
        K k;

        //步骤③:如果节点key存在,直接覆盖value
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //步骤④:如果该bucket位置数据是TreeNode类型,则将新数据添加到红黑树中。
        else if (p instanceof TreeNode)
            e = ((TreeNode)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);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);	//如果链表个数达到8个时,将链表修改为红黑树结构
                    break;
                }
                // key已经存在直接覆盖value
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //更新键值,并返回旧值
        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;
}

步骤总结

①. 判断键值对数组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,如果超过,进行扩容。

JDK1.8有那些变化。

JDK1.7中的HashMap

基于链表+数组实现,底层维护一个Entry数组

Entry[] table;

根据计算的hashCode将对应的KV键值对存储到该table中,一旦发生hashCode冲突,那么就会将该KV键值对放到对应的已有元素的后面, 此时,形成了一个链表式的存储结构,如下图
jdk1.7与1.8HashMap区别_第3张图片

JDK1.8中的HashMap

基于位桶+链表/红黑树的方式实现,底层维护一个Node数组

Node[] table;

在JDK7中HashMap,当成百上千个节点在hash时发生碰撞,存储一个链表中,那么如果要查找其中一个节点,那就不可避免的花费O(N)的查找时间,这将是多么大的性能损失,这个问题终于在JDK8中得到了解决。

JDK8中,HashMap采用的是位桶+链表/红黑树的方式,当链表的存储的数据个数大于等于8的时候,不再采用链表存储,而采用了红黑树存储结构。这是JDK7与JDK8中HashMap实现的最大区别。
如下图所示:
jdk1.7与1.8HashMap区别_第4张图片

异同

共同点

  1. 容量(capacity):容量为底层数组的长度 容量一定为2的次幂
/**
这段代码是用来计算出键值对存放在一个数组的索引,h是int hash = hash(key.hashCode())计算出来的,SUN大师们发现, “当容量一定是2^n时,h & (length - 1) == h % length” ,按位运算特别快 。
源码中大量使用运算,对于计算机,位运算计算效率特别快,毕竟二进制才是亲儿子呀。
 */
static int indexFor(int h, int length) {      return h & (length-1);  }  
  1. 加载因子(Load factor):HashMap在其容量自动增加前可达到多满的一种尺度
    a. 默认加载因子 = 0.75
/**
加载因子越大、填满的元素越多 = 空间利用率高、但冲突的机会加大、查找效率变低(因为链表变长了)

加载因子越小、填满的元素越少 = 空间利用率小、冲突的机会减小、查找效率高(链表不长)
0.75是一个"冲突的机会"与"空间利用率"之间寻找一种平衡与折衷的选择
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f 
  1. 扩容机制:扩容时resize(2 * table.length),扩容到原数组长度的2倍
  2. key为null::若key == null,则hash(key) = 0,则将该键-值 存放到数组table 中的第1个位置,即table [0]
 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

不同点

  1. 发生hash冲突时
    JDK7:发生hash冲突时,新元素插入到链表头中,即新元素总是添加到数组中,就元素移动到链表中。
    JDK8:发生hash冲突后,会优先判断该节点的数据结构式是红黑树还是链表,如果是红黑树,则在红黑树中插入数据;如果是链表,则将数据插入到链表的尾部并判断链表长度是否大于8,如果大于8要转成红黑树。
  2. 扩容时
    JDK7:在扩容resize()过程中,采用单链表的头插入方式,在将旧数组上的数据 转移到 新数组上时,转移操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况 。
    多线程下resize()容易出现死循环。此时若(多线程)并发执行 put()操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环(Infinite Loop),即 死锁的状态 。
    JDK8:由于 JDK 1.8 转移数据操作 = 按旧链表的正序遍历链表、在新链表的尾部依次插入,所以不会出现链表 逆序、倒置的情况,故不容易出现环形链表的情况 ,但jdk1.8仍是线程不安全的,因为没有加同步锁保护。

建议

  1. 使用时设置初始值,避免多次扩容的性能消耗
  2. 使用自定义对象作为key时,需要重写hashCode和equals方法
  3. 多线程下,使用CurrentHashMap代替HashMap

JDK1.7当中HashMap中线程不安全问题有那些?原因分别是什么?

扩容时不安全
线程下resize()容易出现死循环。此时若(多线程)并发执行 put()操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环(Infinite Loop),即 死锁的状态 。

JDK1.8之后如何链地址法,链表长度是多少的时候会转换成红黑树。

长度为8的时候会转换成红黑树

JDK1.8节点个数是多少的时候,红黑树会退回链表。

小于6的时候。

为什么会选择8作为链表转红黑树的阈值。

在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。

HashMap与HashTable有什么区别?

HashTable

  • 底层数组+链表实现,无论key还是value都不能为null,线程安全, 实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
  • 初始size为11,扩容:newsize = olesize*2+1
  • 计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length

HashMap

  • 底层数组+链表实现,可以存储null键和null值,线程不安全
  • 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
  • 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
  • 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
  • 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
  • 计算index方法:index = hash & (tab.length – 1)

你可能感兴趣的:(java,jdk8,java,hashmap)