【JDK源码学习】HashMap源码分析

HashMap在日常开发中使用频率相当频繁,面试中也会经常被问到。这个java中常用的集合类之一,学习下其源码很有必要。

本文会分析HashMap的数据结构、构造函数,常用方法put、get,扩容,链表与红黑树互转。

一、静态属性

/**
* 默认初始容量 - 必须是2的幂
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

/**
* 最大容量。如果构造函数中指定的容量大于这个值,那么会使用该默认值作为最大容量
*/
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;

// 最小树化的容量
static final int MIN_TREEIFY_CAPACITY = 64;

二、构造函数

// 指定初始容量和负载因子的构造方法
public HashMap(int initialCapacity, float loadFactor) {
    // 如果指定的初始容量小于0,直接抛出异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
            initialCapacity);
    // 如果指定的初始大于最大容量,则使用最大容量
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 如果指定的负载因子小于0,或者是非法数字,则直接抛出异常
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
            loadFactor);
    this.loadFactor = loadFactor;
    // 计算table大小,即扩容后的数组大小
    this.threshold = tableSizeFor(initialCapacity);
}

// 指定初始容量,负载因子采用默认
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

// 初始容量和负载因子全部采用默认
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

// 使用一个map去构造HashMap
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

三、get方法

// 其实调用的是getNode方法,如果指定节点为null,则直接返回null,否则返回该节点的value
public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // first是要找的节点所在的数组位置上的第一个元素(数组每个位置放的是一个链表和红黑树)
    // (n - 1) & hash 就是计算这个key位于数组的哪个位置
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        // 如果指定位置上的第一个节点刚好是要找的key,则直接返回第一个节点
        if (first.hash == hash &&
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        // 第一个节点不是要找的key,如果有next节点的话,则继续找
        if ((e = first.next) != null) {
            // jdk8中引入的红黑树,如果第一个节点的树节点
            if (first instanceof HashMap.TreeNode)
                // 采用树的查找算法,查找对应的key
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            // 否则的话,则是链表节点,采用的是循环遍历整个链表,直到找出指定的key
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

四、put方法

// 存入元素,其实调用的是putVal方法
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}


/**
 *
 * @param hash key的hash
 * @param key key
 * @param value 要存入的value
 * @param onlyIfAbsent 如果为true的话,则不改变已存在的值
 * @param evict 如果为false,则table处于创建模式。
 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
    boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 如果table为null,或者长度为0,则进行一次扩容(具体扩容步骤,后面再讲)
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // i为这个key应该放在table的具体位置,如果位置上没有元素,则构造一个节点Node,并且放在这个位置上
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else { // 否则,即是table这个位置已经有元素了
        Node<K,V> e; K k;
        // p就是这个位置上的第一个节点
        // 如果要put进来的key,与第一个节点的key一样,那么将e指向第一个节点
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果第一个节点是树节点,则采用树的插入算法插入一个节点
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else { // 否则,为链表节点
            for (int binCount = 0; ; ++binCount) {
                // 如果该位置只有一个节点,即没有next
                // 注意,这里在for循环里面,且e=p.next,也就是说,e始终会指向最后一个节点
                // 下面会有一个p = e, 新构建的Node会在p.next位置
                // 也就是说,新节点会最终放在链表的最后
                if ((e = p.next) == null) {
                    // 构建一个新节点Node,并将p的next指向这个新节点
                    p.next = newNode(hash, key, value, null);
                    // 如果链表节点数超过的8,则将链表结构转成树结构
                    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) { // existing mapping for key
            V oldValue = e.value;
            // 需要替换,或者旧值为null,则用新的value替换旧值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 后处理,HahsMap中是一个空方法
            afterNodeAccess(e);
            // 返回put的value
            return oldValue;
        }
    }

    // put的元素最终落在链表或者树上的,会走上面的return oldValue,就没有下面的处理的
    // put的元素最终落在table指定位置的第一个节点时,才有下面的处理

    // table的修改次数加1    
    ++modCount;
    // table(数组)的大小大于指定数值后,进行一次扩容
    // 前面说了,threshold就是通过容量和负载因子算出来的,下次扩容的大小
    if (++size > threshold)
        resize();
    // HashMap中是一个空方法
    afterNodeInsertion(evict);
    return null;
}

五、扩容resize方法

final Node<K,V>[] resize() {
    // 用oldTab保存原来的数组table
    // 从这里就能看得出,HashMap底层就是一个数组,数组里的元素就是一个个Node,这个Node可能是TreeNode,也就可能是链表Node
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // 原数组长度
    int oldThr = threshold; // 原扩容阈值
    int newCap, newThr = 0;
    if (oldCap > 0) {
        // 数组长度已经超过最大长度了,直接将下次扩容阈值改成Integer.MAX_VALUE的,然后返回
        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
    }
    // 旧数组长度小于等于0且就扩容阈值大于0,则将新数组长度设为旧的扩容阈值
    // 其实初始化一个HashMap时,走的就是这个分支
    else if (oldThr > 0)
        newCap = oldThr;
    // 否则,新数组长度和扩容阈值都设置默认
    else {
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    // 新的扩容阈值=0,那么就根据数组容量和负载因子,再计算一次新扩容阈值
    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"})
    
    // 上面计算好了扩容之后的table容量
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 下面的计算就是一大推复制操作,宗旨就是把旧table上的元素,全部copy到这个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)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> 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;
                    }
                }
            }
        }
    }
    // 最终返回扩容后的新table
    return newTab;
}

到此为止,HahsMap的源码全部分析完了

题外话,仔细研究里面的扩容操作,就是容量和负载因子算出新数组table的大小,然后将就数组的元素全部复制新数组中。设想一下,如果这复制过程中,有一个线程往这个HashMap中put了一个元素,这个元素会存在哪,会丢失吗?其实这就是面试中经常被问到的,HashMap是线程安全的吗?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值