HashMap源码分析(jdk-8)

284 阅读6分钟

HashMap是我们平时开发经常用的集合,,虽然我们没有那么多的数据需要处理,需要花大时间考虑性能优化,但理解了它的存取数据的方式,对开发也会有所帮助

HashMap是数组加单向链表组成的双列集合, 它允许key为null,value为null。

成员,常量和方法

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16初始容量

static final int TREEIFY_THRESHOLD = 8;//jdk8新增,链表长度>8后,转为红黑树存储

static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量

static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认加载因子

int threshold;// // 扩容的临界值,阈值。size>=threshold就会扩容

final float loadFactor;//加载因子

transient int size;// // 已存元素的个数,map.size()返回的值  

transient int modCount;//结构修改 次数记录

transient Node<K,V>[] table; // hash数组,哈希表,长度设定为2的幂

transient Set<Map.Entry<K,V>> entrySet;//

static class Node<K,V> implements Map.Entry<K,V> {//元素
    final int hash;// 通过hash(Object key)计算出来key的哈希值
    final K key;
    V value;
    Node<K,V> next;}//链表下一个元素

public final int hashCode() {/元素的hasCode /key的hashCode 和 value的hashCode 亦或得到

            return Objects.hashCode(key) ^ Objects.hashCode(value);

        }

static final int hash(Object key) {//计算key的哈希值的方法:key的hash会经扰动函数,避免hash取余后在数组index相同的几率,让高位也参与hash生成


      int h;
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

构造方法

构造方法共有4个,其中带2参的此构造方法包含了主要功能,在此只分析这一个(
构造一个带指定初始容量和加载因子的空 HashMap)

//参数一,初始容量。参数二,加载因子
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)//检查参数合法性
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)//容量最大1<<30,10亿左右
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))//iaNaN()检查float合法性
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;//设置加载因子
    this.threshold = tableSizeFor(initialCapacity);//计算并设置阈值
}

put方法

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);//调用下一个方法
}

//onlyIfAbsent: 如果该位置存在value,put方法是否覆盖它
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; //哈希表
    Node<K,V> p;
    int n, i;//n为哈希表长度。i为传入的key再表中位置
    if ((tab = table) == null || (n = tab.length) == 0)//如果是初次
        n = (tab = resize()).length;//走初始化方法,并将新建表的长度赋值给n
    if ((p = tab[i = (n - 1) & hash]) == null)//(n - 1) & hash是计算哈希值在表中位置,与hash%n等价.此处将表此位置的元素赋给p,在p==null的if条件小
        tab[i] = newNode(hash, key, value, null);//直接将表的该位置存储该元素
    else {//如果该位置元素不为null,下面会判断是该替换还是在链尾追加
        Node<K,V> e; K k;//e用来存旧位置的元素,k用来存就位置的key
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))//key相同。k存上了旧位置的key
            e = p;//e存上了旧位置的元素
        else if (p instanceof TreeNode)//key不同,且是红黑树节点,走红黑树逻辑的put方法,暂不深究
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {//key不同,且还是普通链表
            for (int binCount = 0; ; ++binCount) {//for循环无终止条件
                if ((e = p.next) == null) {//e存上了p的next()且其为null的条件下(是尾了)
                    p.next = newNode(hash, key, value, null);//链尾加上新值
                    if (binCount >= TREEIFY_THRESHOLD - 1) // 如果长度超过8,链转为红黑树,不深究
                        treeifyBin(tab, hash);
                    break;
                }
                //p不是链尾,且,e的key跟put的key相同,会走到下边的e.value = value;
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;//e的key跟put的key不同,p置为新的e,继续下一轮循环
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;//e的value设为传进来的value
            afterNodeAccess(e);//此方法空实现,埋给linkedhashmap用的
            return oldValue;//return旧的value,且此情况下modcount没发生改变
        }
    }
    //走到以下的条件为,是追加新的节点,而非替换value
    ++modCount;//此时预示着结构改变了(非替换旧value的情况下)
    if (++size > threshold)//如果增加一个,后的size>阈值
        resize();//表重构
    afterNodeInsertion(evict);//空实现
    return null;//
}

重构大小

//返回新重构后的数组哈希表
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;//
    int oldCap = (oldTab == null) ? 0 : oldTab.length;//旧的数组长度
    int oldThr = threshold;//旧的阈值
    int newCap, newThr = 0;//新的数组长度,阈值
    if (oldCap > 0) {//旧的数组长度>0
        if (oldCap >= MAXIMUM_CAPACITY) {///如果上限了
            threshold = Integer.MAX_VALUE;//阈值调为最大值
            return oldTab;//哈希表不再扩张了
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&//新数组长度扩大一倍,

                 oldCap >= DEFAULT_INITIAL_CAPACITY)//并且旧的数组长度>16条件下
            newThr = oldThr << 1; // double threshold新的阈值也扩大一倍
    }
    else if (oldThr > 0) //旧的数组长度=0 且旧的阈值>0
        newCap = oldThr;//新数组长度=旧的阈值长度
    else {// zero initial threshold signifies using defaults,第一次旧的数组长度为0条件下
        newCap = DEFAULT_INITIAL_CAPACITY;//16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新的阈值设为0.75x16=12
    }
    if (newThr == 0) {//新的阈值仍=0
        float ft = (float)newCap * loadFactor;//新的表长度x加载因子
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?//在最大值范围内
                  (int)ft : Integer.MAX_VALUE);//赋值给新的阈值
    }
    threshold = newThr;//新的阈值赋值到成员阈值中
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//利用新计算的哈希表长度new个新表
    table = newTab;//新哈希表赋给成员哈希表
    if (oldTab != null) {//非空条件下,开始高低位移动了
        for (int j = 0; j < oldCap; ++j) {//for循环旧哈希表
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {//e暂存数组的第一个元素
                oldTab[j] = null;//旧表置空,优化内存
                if (e.next == null)//如果下一个元素为空
                    newTab[e.hash & (newCap - 1)] = e;//存到新表的相应位置
                else if (e instanceof TreeNode)//树,不看了
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { ///如果下一个元素非空,要高低位移动了 

                    Node<K,V> loHead = null, //低的头loTail = null;//低位的尾
                    Node<K,V> hiHead = null, //高的头hiTail = null;//高的尾
                    Node<K,V> next;
                    do {
                        next = e.next;//暂存下一个元素到next
                        if ((e.hash & oldCap) == 0) {// (等于0代表小于oldCap,应该存放在低位,否则存放在高位

)                             if (loTail == null)
                                loHead = e;//赋值e到低头
                            else
                                loTail.next = e;//低尾的下一个存上e
                            loTail = e;//赋值e给新低尾
                        }
                        else {//如果不是在哈希表0位
                            if (hiTail == null)//高尾为空时
                                hiHead = e;//e赋值给高头
                            else//高尾非空时
                                hiTail.next = e;//高尾的下一个存上e
                            hiTail = e;//赋值e给新的高尾
                        }
                    } while ((e = next) != null);//e下一个不为空时,继续循环
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;//跟扩容前相同的位置存上低头
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;//扩容后的位置: 存上高位的头
                                         high位= low位+原表长度

                    }
                }
            }
        }
    }
    return newTab;//返回新表
}

总结

数组的扩容永远为2的倍数, << 1; // double方便取余位运算%
jdk8,>8后转为红黑树
遍历顺序:数组+链表
根据索引查找元素(getKey)的时候,两步:1,通过hasHash(Key)快速的找到数组哈希表索引,2慢速的遍历链表
扩容时建一张新表,并将旧表数据全部移到新表.:low位索引不变,top位索引等于原索引+原数组长度

zyt