源码学习(3) HashMap源码分析-1 添加对象

264 阅读6分钟

代码版本: JDK 1.8

都是集合框架,和上篇文章思路一样。

介绍: HashMap底层实现由之前的【数组+链表(1.7之前)】改为【数组+链表+红黑树(1.8)】。看到数组和链表是不是有点似曾相识,没错,ArrayList LinkedList就用了这俩东西,表面是要学习个新的集合框架,但是却只换了个皮。废话不多说了,看源码。

  1. 构造函数(先看两个构造函数)
// 当然先看最简单的了,就一句话
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) // [MAXIMUM_CAPACITY = 1 << 30]
        initialCapacity = MAXIMUM_CAPACITY; // 只能装这么多
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    this.loadFactor = loadFactor; // 初始化一个负载因子,这是干啥的??后面再说👎
    this.threshold = tableSizeFor(initialCapacity); // 临界值(容量*负载因数)
}
  1. 增加元素
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
// 这一坨代码,看起来是不是有点晕 +_+ 先放在这不看,略过此段代码!!
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // 👇
    Node<K,V>[] tab;
    Node<K,V> p;
    int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        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) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                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;
}

增加第一个元素

// 现在只看增加第一个元素时走的代码逻辑,是不是心情一下就变好了 ^_
// 此番逻辑我只是按照在第一次添加元素的时候走的
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab;  //定义底层数组 这就是hashmap底层的数组了
    Node<K,V> p;   // 链表
    int n, i;
    if ((tab = table) == null || (n = tab.length) == 0) // 第一次添加元素,table为null
        // 👇 快去看下resize()方法,  ▲▲ 看完resize()继续看这里,此时n = 16了
        n = (tab = resize()).length;  // 集合大小
    // 不看resize不知道tab是啥,那就看吧(又是一坨Ⅹ) 
    // n-1 为啥馁? 因为计算机扩容基本都是二倍增长的,所以这里为了增加hash散列度
    if ((p = tab[i = (n - 1) & hash]) == null) 
        //他来了他来了!i 是hash后的一个值,就是经常说的取模运算了。并实例化新节点,给tab
        tab[i] = newNode(hash, key, value, null);
    return null;
}
// 老规矩,减小难度,还是只看第一步走的逻辑(多余代码我删除了),那就开始吧
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table; // 此时table还是null,所以oldTab也是null
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // 这里就是0了
    int oldThr = threshold; // 因为我们使用的是无参构造,这里也是默认的,所以还是0,简单吧
    int newCap, newThr = 0; // 还是 0 
    // 这里有点突如其来的感觉,是因为很多没走的判断被我干掉了
    // 默认初始容量 1 << 4 = 16 第一个不是0的地方,注意喽,面试可能会用到。
    newCap = DEFAULT_INITIAL_CAPACITY;  
    // 在第三个构造方法时提到的 负载因子(默认0.75)和临界值(集合扩容的标志)
    // 0.75 * 16 = 【12】
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 
    // 新的临界值赋值给临界值属性,此处是默认情况下,所以为12
    threshold = newThr; 
    // ★★★   初始化一个大小为16的Node数组,赋值给table并返回
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 
    table = newTab;
    return newTab;  // 👆 回去继续看putVal()方法了。
}

至此,第一个元素就加进去了。 总结一下:

增加第一次元素最重要的地方以下几点:

  • 默认初始容量是数组的容量为16,扩容会根据map的size和threshold比较做扩容
  • 默认的负载因子是0.75f
  • 临界值为【容量*负载因子】
  • 最后一个是hash取模运算那里

下面看添加第二个元素 —> 开始

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab;
    Node<K,V> p;
    int n, i;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    return null;
}

第二轮就是到此一游,啥收获没有。。


继续。我们的目标是hash碰撞,看看这么高大尚的东西到底是个啥东西。
经过一番寻找,终于找到碰撞的key了 【在容量为16的情况下 碰撞的key 2,18,34】

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 没碰撞的情况
    if ((p = tab[i = (n - 1) & hash]) == null) 
        tab[i] = newNode(hash, key, value, null);
    else { // 发生碰撞 走else代码块
        Node<K,V> e; // 这是装碰撞节点的东西
        K k; // 存被碰瓷的那个key
        // 此时p是被碰瓷的那个节点
        // 这个if判断的是 如果 hash相同 并且key也是一样,就直接
        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) {
            // 循环找到碰瓷的最后一个节点,就是单链表的最后一个,因为hash碰撞是在尾部添加的
            // 最后一个节点才会进入if
                if ((e = p.next) == null) {
                    // 碰瓷成功,这是追尾了,跟在屁股后面挂着
                    p.next = newNode(hash, key, value, null);
                    // 这里转换成的红黑树。 ★,这次先这样,在搞一坨恐怕受不了。无视它
                    if (binCount >= TREEIFY_THRESHOLD - 1) 
                        treeifyBin(tab, hash);
                    // 碰完了,就赶紧跑路吧。
                    break;
                }
                /** 如果循环到了最后一个节点,此代码不会执行了。
                  * 走到这,e不是空值了。
                  * 这里遇到相同的key就会停止循环,去走覆盖值的逻辑。
                  */
                if (e.hash == hash &&((k = e.key)== key || (key != null && key.equals(k))))
                    break;
                p = e; // 相当于把p.next赋值给p  (e = p.next)
            }
        }
        // 存在相同的key 覆盖相同key e在这里如果不为空,那就是碰到了key相同的情况了 
        // e的值就是与 来碰瓷的那个key值相同的那个节点,
        if (e != null) {  
            V oldValue = e.value;
            // onlyIfAbsent一般都是false,覆盖,putIfAbsent() 这个方法是true,这里不展开了
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;  // 覆盖。
            afterNodeAccess(e);
            return oldValue;
        }
    }
    return null;
}

至此,hash碰撞算是完了。

再来总结一番:

  • 碰瓷的太多的时候,就会生成红黑树。 临界值是TREEIFY_THRESHOLD = 8
  • 碰瓷的都是追尾模式,就是在last节点添加元素。
  • key相同(覆盖原来的值),---在这里面putIfAbsent()应该比较特殊。
  • 采用的一个无限for循环,处理链表碰撞的判断,碰到key相同的,覆盖,不相同的继续循环,直至最后一个节点,在屁股上添加新的节点。

把找相同hash key的代码贴一下

public static void main(String[] args) {
    for (int i =0; i<200;i++) {
        if (2 == (15 & hash(i))) { // 这个15是map容量-1
            System.out.print(i + "    ");
        }
    }
}
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
输出::::
2    18    34    50    66    82    98    114    130    146    162    178    194 

待续...

接下来还有 >>>>>>>>>>>>>>>

  • 红黑树
  • 查询元素
  • 删除元素
  • 修改元素