资讯专栏INFORMATION COLUMN

jdk1.8中HashMap源码解析

李文鹏 / 334人阅读

摘要:当链表长度即将超过阀值,会把链表转化为红黑树。然后再判断是链表还是红黑树如果值相同,并且相同表示数组中第一个元素即为相同的将数组中第一个元素赋值给如果当前元素类型为表示为红黑树,返回待存放的。

前提:学习HashMap的底层代码之前,首先要对数据结构要个大致的了解。其中重点了解数组,链表,树的概念和用法。

一.图示分析HashMap的结构

(1)图示为JDK1.8之前的HashMap结构。数组+链表,数组中的元素为链表的头节点。如果不同的key对应相同的hash值,则会在头节点后形成链表。
通过代码的实现,我们可以分析出:如果在储存数据时,某一个链表过长,则会影响查询性能。(下面会分析put和get方法,解释链表过长如何影响性能)

(2)JDK1.8中进行了优化。当链表长度即将超过阀值(TREEIFY_THRESHOLD),会把链表转化为红黑树。底层实现变为数组+链表+红黑树

二.通过代码分析底层结构中元素的结构和原理

(1)数组和链表中的每个元素都是Node对象,它是HashMap的一个内部类并且实现了Map.Entry。其中包含了几个重要属性,int hash; K key; V value; Node next;

static class Node implements Map.Entry {
    final int hash;//通过key的hashCode方法获取的hash值。hash值相同的key会在数组中找到同一个位置
    final K key;
    V value;
    Node next;//指向下一个节点,储存了下一个节点的地址
  
    Node(int hash, K key, V value, Node 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; }
 
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }
 
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
    
    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;
    }
}
   

(2)当链表长度达到阀值,链表转化为红黑树。此时数组和红黑树中的元素是TreeNode

static final class TreeNode extends LinkedHashMap.Entry {
    TreeNode parent;  // 父节点
    TreeNode left; //左子树
    TreeNode right;//右子树
    TreeNode prev;    // needed to unlink next upon deletion
    boolean red;    //颜色属性
    TreeNode(int hash, K key, V val, Node next) {
        super(hash, key, val, next);
    }
}

(3)HashMap中几个常用的属性和构造方法

public class HashMap extends AbstractMap implements Map, Cloneable, Serializable {
    // 序列号
    private static final long serialVersionUID = 362498820763181265L;    
    // 默认的初始容量是16,通过移位运算获得结果。在计算机底层位运算速度最快
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  
    // 最大容量 2的30次方
    static final int MAXIMUM_CAPACITY = 1 << 30; 
    // 默认的加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 链表转红黑树的阀值
    static final int TREEIFY_THRESHOLD = 8; 
    // 扩容时红黑树转链表的阀值
    static final int UNTREEIFY_THRESHOLD = 6;
    // 链表转化为红黑树对应的table的最小值
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 存储节点的数组,一定是2的幂次方
    transient Node[] table; 
    //
    transient Set> entrySet;
    // 数组中元素的个数,注意这个不是数组的长度。
    transient int size;
    // 修改HashMap的次数
    transient int modCount;   
    // 阀值,当数组中的元素大于threshold时HashMap进行扩容 threshold = (数组长度)capacity * loadFactor
    int threshold;
    // 填充因子
    final float loadFactor;
    
    //无参构造函数,比较常用
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; 
    }
    
    //一个参数,数组容量
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    
    //两个参数,数组容量和加载因子
    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;
        //因为我们的入参指定了数组大小,但是传入的值可能不满足HashMap要求。
        //因此HashMap使用tableSizeFor保证数组大小一定是2的n次幂      
        this.threshold = tableSizeFor(initialCapacity);
    }
    /**
      此方法的目的是:保证有参构造传入的初始化数组长度是>=cap的最小2的幂次方。
      对n不断的无符号右移并且位或运算,可以将n从最高位为1开始的所有右侧位数变成1,
      最后n+1即成为大于cap的最小2的幂次方。
      第一行 n=cap-1 是为了保证如果cap本身就是2^n,那么结果也将是其本身
    */
    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;
    }

    //传入一个HashMap实例
    public HashMap(Map m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
    
    //如果传入的HashMap中有元素,遍历它并且把其中的元素put到当前HashMap中  
    final void putMapEntries(Map m, boolean evict) {
        int s = m.size();
        if (s > 0) {        
            if (table == null) { //当前数组为空           
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                        (int)ft : MAXIMUM_CAPACITY);          
                if (t > threshold)
                    threshold = tableSizeFor(t);//计算数组长度,并且保证长度是2的幂次方
            }      
            else if (s > threshold)
                resize();        
            for (Map.Entry e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }
    
}

(4)put方法解析

    /**如果key等于null就返回0,因此key为null时的数据存储在数组中的第一个位置
    *如果key不为null,将key的hashCode值赋值给h。然后将h无符号位移16位后再和该key的hashCode值进行异或
    *作用:将hashCode的高16位和低16位进行异或,混合hashCode的高位和低位。以此加大低位的随机性。
    *当在计算key的位置时,(n - 1) & hash对数组长度取模,如果只取低位会造成大量碰撞。所以进行高低位异或。
    */
     static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
           
    /*当多个key的hash值相同时,会形成链表。如果此时又在该链表上插入一个元素,就要遍历整个链表对比是否有相同的key,如果有则直接
    *替换,如果没有找到链表尾部,插入新元素。因此在JDK1.7中链表过长,引起效率低下    
    */    
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //定义变量                   
        Node[] tab; Node p; int n, i;
        //将table数组赋值给tab,将数组长度赋值给n。判断当前map中的table数组是否为空(第一次肯定为空)
        if ((tab = table) == null || (n = tab.length) == 0)            
            n = (tab = resize()).length;//将扩容后的数组赋值给tab,然后获取数组长度赋值给n
        if ((p = tab[i = (n - 1) & hash]) == null)//计算key在数组中的位置,然后判断该位置是否为null并赋值给p
            tab[i] = newNode(hash, key, value, null);//该位置没有元素,直接创建一个节点放入
        else {//进入下面代码,表示该位置有元素。然后再判断是链表还是红黑树
            Node e; K k;
            //如果hash值相同,并且key相同表示数组中第一个元素即为相同的key
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;将数组中第一个元素赋值给e
            else if (p instanceof TreeNode)//如果当前元素类型为TreeNode表示为红黑树,putTreeVal返回待存放的Node。
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); e可能为null
            else {//链表结构,遍历链表找到插入的位置
                for (int binCount = 0; ; ++binCount) {
                    //遍历至链表尾部,如果没有相同的key,则插入尾部一个节点
                    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直接跳出循环
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;                        
                    p = e;
                }
            }
            if (e != null) { // e不为null,代表存在相同的key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//将新值直接覆盖老值
                afterNodeAccess(e);//一个空方法
                return oldValue;//返回老值
            }
        }
        //代码走到这里说明,数组中新增了一个Node
        ++modCount;//修改HashMap的次数
        if (++size > threshold)//如果增加节点后,数组中元素的个数大于了扩容的阀值,则进行扩容
            resize();
        afterNodeInsertion(evict);//一个空方法
        return null;//第一次增加节点,返回null
    }
    

(5)get方法解析

    //调用getNode方法返回一个Node节点并赋值给e。如果e为null,则返回null。否则返回Node节点中的value属性
    public V get(Object key) {
        Node e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    //hash(key)计算获取hash,即key位于数组中的位置
    final Node getNode(int hash, Object key) {
        Node[] tab; Node first, e; int n; K k;
        //取值前保证当前map中的table不为null,并且该key所在数组中的Node不为null,如果不满足条件直接返回null
        if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))//先检查第一个Node
                return first;//如果第一个Node满足条件,证明这个就是我们要找的元素,直接返回
            if ((e = first.next) != null) {//上面条件不满足,则从红黑树,链表中找元素
                if (first instanceof TreeNode)//如果该节点属于TreeNode,则从红黑树中查找
                    return ((TreeNode)first).getTreeNode(hash, key);
                do {//遍历链表找到相同的key则返回元素,否则跳出循环,返回null
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }
    

(6)resize方法解析

    
     //resize()是HashMap实现扩容机制的核心。在put方法中出现两次,第一次初始化数组时,第二次数组元素达到阀值时。
     //将原来数组中的元素重新hash到新的位置。新的位置和原位置相同,或者是原来位置的两倍      
     final Node[] resize() {
        Node[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        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"})
            Node[] newTab = (Node[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node 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) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    

未完待续

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/75134.html

相关文章

  • java源码

    摘要:集合源码解析回归基础,集合源码解析系列,持续更新和源码分析与是两个常用的操作字符串的类。这里我们从源码看下不同状态都是怎么处理的。 Java 集合深入理解:ArrayList 回归基础,Java 集合深入理解系列,持续更新~ JVM 源码分析之 System.currentTimeMillis 及 nanoTime 原理详解 JVM 源码分析之 System.currentTimeMi...

    Freeman 评论0 收藏0
  • JDK1.8HashMap部分源码解析

    摘要:概述主要来存放键值对。之前使用数组链表的形式,之后进行了改变,使用了数组链表或者红黑树的形式。如果为,则按照字段中保存的初始容量进行分配。并且之前在中的元素应呆在原处或者移动到倍位置处。 概述 HashMap主要来存放键值对。JDK1.8之前使用数组+链表的形式,JDK1.8之后进行了改变,使用了数组+链表或者红黑树的形式。 小概念普及 关系运算简介 0 0 0 1 1 1 ...

    DandJ 评论0 收藏0
  • Java集合之HashMap源码解析

    摘要:之前,其内部是由数组链表来实现的,而对于链表长度超过的链表将转储为红黑树。非线程安全,即任一时刻可以有多个线程同时写,可能会导致数据的不一致。有时两个会定位到相同的位置,表示发生了碰撞。 原文地址 HashMap HashMap 是 Map 的一个实现类,它代表的是一种键值对的数据存储形式。 大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashM...

    lindroid 评论0 收藏0
  • 集合源码学习之路---hashMap(jdk1.8)

    摘要:值得位数有的次方,如果直接拿散列值作为下标访问主数组的话,只要算法比较均匀,一般是很难出现碰撞的。但是内存装不下这么大的数组,所以计算数组下标就采取了一种折中的办法,就是将得到的散列值与数组长度做一个与操作。 hashMap简单介绍 hashMap是面试中的高频考点,或许日常工作中我们只需把hashMap给new出来,调用put和get方法就完了。但是hashMap给我们提供了一个绝佳...

    kamushin233 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<