面试储备资料我爱编程Java集合源码分析

HashMap1.8 源码解析(1)--插入元素

2018-06-19  本文已影响33人  nicktming

所有代码:https://github.com/nicktming/code/tree/dev/java/collection_source_code/HashMap_put

常量和属性和节点

常量
    /* 默认的数组的长度 或者说默认是buckets/bins的长度 */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    /* 最大长度 */
    static final int MAXIMUM_CAPACITY = 1 << 30;
    /* 默认的加载因子 用于计算阈值(threshold) */
    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;

属性

    /*为什么有些属性设置为transient 在说序列化的时候会讲解 */
    /* 用于存节点的数组(节点会hash到table中的某一个index) */
    transient Node<K, V>[] table;
    /* 用于存HashMap中的所有节点 */
    transient Set<Map.Entry<K, V>> entrySet;
    /* 节点的总个数 */
    transient int size;
    /* 修改的次数 后续会看到modCount的作用 */
    transient int modCount;
    /* 决定是否扩容的阀值 */
    int threshold;
    /* 加载因子 用于计算阈值(threshold) */
    final float loadFactor;
节点

继承于Map.Entry<K,V>,这里就不贴它的代码了,实现它的几个方法即可

   static class Node<K, V> implements Map.Entry<K, V> {
        final int hash;  // 节点的hash值
        final K key;     // 节点的键
        V value;         // 节点的值
        Node<K, V> next; // 指向下一个节点的指针
        
        /* 构造函数 */
        Node (int hash, K key, V value, Node<K, V> next) {
            this.hash  = hash;
            this.key   = key;
            this.value = value;
            this.next = next;
        }
        
        /* 重写getKey()方法 */
        @Override
        public K getKey() {
            // TODO Auto-generated method stub
            return key;
        }
        
        /* 重写getValue()方法 */
        @Override
        public V getValue() {
            // TODO Auto-generated method stub
            return value;
        }
        
        /* 重写setValue()方法 */
        @Override
        public V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        
        /* 比较当前的节点与对象o是否属于同一个对象 final方法表示子类不能重写 */
        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;
        }
    }

构造函数

既然已经了解了基本的属性和节点是如何表示的,我们看看它的三个
基本构造函数.

    /* 请注意并没有initialCapacity这个属性, 所以如何给数组初始化多大长度呢? 
     * 下面的分析的resize()方法会给出答案.
     * */
    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))  //检查loadFactor是否有异常
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;                    //赋值loadFactor操作
        this.threshold = tableSizeFor(initialCapacity);  //根据初始容量计算出阀值
    }
    
    public HashMap(int initialCapacity) {  
        this(initialCapacity, DEFAULT_LOAD_FACTOR); //采用默认加载因子调用第一个构造函数
    }
    
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // 其余的属性都采用默认值
    }
    /* 返回值是必须是2的幂 这个方法的返回值是大于等于cap的最小的2次幂
     * 至于为什么必须是2的幂?在后续解析的hash和resize()会看到答案
     * */
    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;
    }
image.png

插入

接下来看看put方法

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
    /* put 方法 */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    
    /**
     * hash  : key的hash值
     * onlyIfAbsent : 为true的时候不改变原有值 为false时用value取代旧值 (在代码中会有体现)
     * evict : 在HashMap的子类LinkedHashMap中会有用 到时候分析LinkedHashMap可以看到它的具体作用
     * 
     * */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
        Node<K, V>[] tab;
        Node<K, V> p;
        int n, i;
        /** 如果tab还没有创建数组的话,则需要去resize方法中创建数组 
         * resize()放到接下来解析 先继续看后面的逻辑
         * */
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        /**
         * 1. 首先根据hash值计算出该节点属于哪个bucket/bin,也就是index
         *    i = (n - 1) & hash 其实就等于 hash%n (用位操作会快一点,这是n为2次幂的第一个用处)
         *    假设 n = 16, hash = 31 -> i = 00001111&00011111=00001111=15
         * 2. 如果此时的bucket是空,表明这个bucket还没有任何节点存入,
         *    因此生成新节点后直接放入到该bucket
         */
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            /**
             * 进入else模块就说明 p此时不为null,所以这个节点应该放到这个bucket的后面
             * 接下来如果这个节点之前有插入过,就会节点赋给e
             * 有三种情况(互斥条件,要么1要么2要么3出现):
             * 1. 此节点已经存在并且就在bucket的第一个位置,直接把p赋给e
             * 2. p是一个TreeNode节点,(TreeNode是Node的一个间接子类,红黑树的分析会专门放到一个博客分析)
             *    那表明这个bucket已经红黑树树化了,因此调用红黑树树化去插入或者更新
             * 3. 在链表中,所以直接遍历链表即可,有一点需要注意,如果是新增加的节点要么链表中的数量就会增加一个
             *    有可能会达到阀值,一旦到达阀值就需要调用treeifyBin方法树化,至于会不会树化已经怎么树化后面会
             *    解析,这里先有个概念理解逻辑就可以
             */
            Node<K, V> e; // 如果这个节点已经存入过,就会拿出那个节点并且赋给e
            K k;
            /* 情况1 */
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)  /* 情况2 */
                e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
            else {
                /* 情况3 */
                for (int binCount = 0;; ++binCount) {             // 遍历链表并且记录链表个数
                    if ((e = p.next) == null) {                   // 从链表的第二个开始,因为p在第一种情况已经比较过了
                        p.next = newNode(hash, key, value, null); // 插入到链表尾
                        if (binCount >= TREEIFY_THRESHOLD - 1)    // 树化 这个时候就可以看到TREEIFY_THRESHOLD的作用了
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) //表明链表中已经存在了这个节点
                        break;
                    p = e;
                }
            }
            if (e != null) {    // e不为null表明此节点已经存入过 所以这里没有modCount++
                V oldValue = e.value;
                //这里就可以很明确的看到onlyIfAbsent的作用,对了如果oldValue为null,那onlyIfAbsent就不起作用了
                if (!onlyIfAbsent || oldValue == null) 
                    e.value = value;
                afterNodeAccess(e); // 用于子类LinkedHashMap的方法
                return oldValue;
            }
        }
        /**
         *  进行到这里之前没有此(节点或者说key也行)存入过
         */
        ++modCount;                // modCount++
        if (++size > threshold)    // 先增加size,mappings的个数 判断是否需要扩容
            resize();
        afterNodeInsertion(evict); // 用于子类LinkedHashMap的方法
        return null;
    }
    /* 将hash对应的bucket链表红黑树树化*/
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        /**
         *  有两个条件会先采用扩容而不是直接树化
         *  1. tab为null
         *  2. tab的length也就是capacity的大小比MIN_TREEIFY_CAPACITY=64小
         *     因为这个时候认为扩容的效果比树化要好
         */
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) 
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) { //如果当前bucket不为null
            TreeNode<K,V> hd = null, tl = null;
            /**
             * 循环遍历整个链表
             * 1. 先把Node节点转换成TreeNode节点
             * 2. 红黑树的所有节点按原来的顺序利用指针(prev和next)形成了一个双向链表
             *    这也是多次遍历链表的时候顺序也不会变化的原因,后续有专门的一个博客来分析HashMap的遍历
             */
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            /**
             * 将TreeNode形成的双向链表转化成红黑树
             */
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }
    
      // 将Node节点转化成TreeNode节点
    TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    }
    
    // Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }

扩容

HashMap中的数量超过了阀值后,会进行扩容,代码是最好的教材,直接看代码

/* 扩容 */
    final Node<K,V>[] resize() {
        /**
         * oldTab 是 table
         * oldThr 是 旧阀值
         * oldCap 是 以前table的length,如果以前是null就为0
         * 大情况为两种情况(有初始化过和第一次初始化)
         * 1. 有初始化过: 这个时候oldCap会比0大
         * 2. 第一次初始化: 分两种小情况
         *    2(1). 有threshold值,其实也就是从构造函数中用initCapacity计算出来的threshold
         *    2(2). 没有threshold,对应的空构造函数.
         */
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {  // 对应情况1
            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) // 对应情况2(1).有参的两个构造函数会进入这个分支
            newCap = oldThr; // 这个知道为什么没有initCapacity也可以给table初始化长度了,newThr没有赋值
        else {               // 对应情况2(2).无参构造函数会进入这个分支
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {  // 如果新的阀值为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<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //初始化数组
        table = newTab;     //设置一下新table
        /**
         * 1. 如果第一次初始化的时候就直接返回了
         * 2. 不是的话就需要把所有在oldTab上的元素转移到新的table中
         */
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    /**
                     * 表明这个bucket里面有节点
                     * 分为三个情况:
                     * 1. 如果这个bucket只有一个节点, 那直接rehash一下放到新的table中就可以了
                     * 2. 如果是树节点,那就由红黑树迁移(会写一个专门的博客分析HashMap的红黑树)
                     * 3. 如果是链表,那就遍历链表转移,如何转移呢?
                     *    首先简单介绍一下rehash,因为n=oldCap=table.length是2的幂,hash=key.hash&(n-1)
                     *    由于新的table.length是2*oldCap,所以新hash=hash&(2*n-1)
                     *    用二进制位表示(2n-1)也就是在以前的基础上加了一个1,所以与操作的时候就看key.hash的对应的那个位置是0还是1
                     *    如果是0:那rehash的结果就没有改变
                     *    如果是1:那rehash的结果就在原有的hash的结果上加上oldCap就可以了
                     *    
                     *    转移的时候把原来的链表分成两个链表:
                     *    3(1): 结果为0的链表-loHead
                     *    3(2): 结果为1的链表-hiHead
                     *    最后两个链表头放到table对应的bucket中
                     * 
                     */
                    oldTab[j] = null;
                    if (e.next == null)                    // 情况1
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)        // 情况2
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else {                                 // 情况3
                        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) {  //情况3(1)
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {                         //情况3(2)
                                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;
    }
rehash例子说明

对于rehash的部分我在解释里面说了下,接下来我将用一个例子和一张图来给大家说明一下.

先定义了一个Person类,重写了hashcodeequals方法,目的是我想让插入的元素放到同一个bucket中.

public class Person {
    String name;
    int age;
    public Person() {}
    public Person(String name, int age) {
        this.name = name;
        this.age  = age;
    }
    
    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
    @Override
    public boolean equals(Object obj) {

        if (this == obj) return true;
        if (obj instanceof Person) {
            Person p = (Person)obj;
            return p.name.equals(this.name);
        }
        return false;

        //return true;
    }
    @Override
    public int hashCode() {
        // TODO Auto-generated method stub
        return age;
    }
}

测试代码,我把HashMap的部分代码摘了出来放在我自己的项目里面,所以可以直接访问table和里面的元素并且打印了table中对应的元素.

public class TestHashMap {

    public static void main(String[] args) {
        HashMap<Person, Integer> map = new HashMap<>(3);
        map.put(new Person("tom_1", 12), 12);
        map.put(new Person("tom_2", 0), 0);
        map.put(new Person("tom_3", 4), 4);
        System.out.println("capacity:" + map.table.length);
        printMap(map);
        map.put(new Person("tom_4", 16), 4);
        System.out.println("------------------after insert tom_4---------------------");
        System.out.println("capacity:" + map.table.length);
        printMap(map);
    }
    
    private static void printMap(HashMap<Person, Integer> map) {
        HashMap.Node<Person, Integer>[] table = map.table; 
        for (int i = 0; i < table.length; i++) {
            System.out.print(i + ":");
            HashMap.Node<Person, Integer> e;
            if ((e = table[i]) != null) {
                System.out.print(e);
                HashMap.Node<Person, Integer> p;
                while ((p = e.next) != null) {
                    System.out.print("->" + p);
                    e = e.next;
                }
            }
            System.out.println();
        }
    }
}

测试结果


image.png

对应的图片


rehash(1).png

所有代码的流程图我就没有化了,相信如果认认真真看了代码的注释,一定可以看明白的.另外如果哪里有问题,欢迎大家指正哈.

我的测试代码和一部分代码都放到了github上面了,如果有兴趣可以下载下来测试看看HashMap的一系列机制和属性.

1.HashMap1.8 源码解析(1)--插入元素
2.HashMap1.8 源码解析(2)--删除元素
3.HashMap1.8 源码解析(3)--TreeNode(红黑树)包括每一个方法
4.HashMap1.8 源码解析(4)--遍历元素

上一篇下一篇

猜你喜欢

热点阅读