HashMap源码解析

HashMap、HashTable、ConcurrentHashMap总结:

本篇博文将会大篇幅介绍 JDK1.8 HashMap,下面总结:

HashMap:
  1. JDK1.7底层是 数组 + 链表实现的, JDK1.8添加了红黑树,节点达到一定条件之后,链表和红黑树之间存在相互转化的场景
  2. key 不可以重复,但可以为null,value值不做限定。
  3. HashMap数组初始化size=16,每次扩容为2倍,size一定为2的n次幂【初始化时传入的size,会被转化为2的n次幂】
  4. 默认情况下,当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
  5. 哈希冲突:若干Key的哈希值按数组大小取模后【hash & (tab.length – 1)】,如果落在同一个数组下标上,将组成一条Entry链【红黑树】,对Key的查找需要遍历Entry链上的每个元素执行equals()比较
  6. 加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容
  7. 空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率
HashTable:
  1. 底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
  2. 初始size为11,扩容:newsize = olesize*2+1
  3. 计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
ConcurrentHashMap:
  1. ConcurrentHashMap使用了锁分离的技术实现线程安全,可以完全替代HashTable,在并发编程的场景中使用频率非常之高。
  2. JDK1.7 ConcurrentHashMap 是使用Segment段来进行加锁,个段就相当于一个HashMap的数据结构,每个段使用一个锁, JDK1.8之后Segment虽保留,但已经简化属性,仅仅是为了兼容旧版本,使用和HashMap一样的数据结构每个数组位置使用一个锁。
HashMap源码解析:

上面我们已经讲了 HashMap 是数组、链表、红黑树组成的,我们把他抽象成一个图可以看到,一定要记住这个图片,其中每个方块是一个节点,每个节点里面的字母是 key:
在这里插入图片描述
打开 HashMap的源码会发现其中定义了很多变量,其中有几个比较重要的变量:

  1. Node<K,V>[] table : node 类型的数组,作为HashMap的三大结构之一
  2. int size: 已储存元素的个数
  3. int threshold: 扩容的阈值,当 HashMap的size大于threshold时会执行resize操作。 threshold=table .length * loadFactor
  4. float loadFactor: 负载因子, 用来计算 threshold

	// 默认初始容量-须是2的幂 这里是 2的4次方16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

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

    // 默认负载系数 (HashMap扩容是使用)
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 链表转化为红黑树的阈值
    static final int TREEIFY_THRESHOLD = 8;

    // 红黑树中节点个数转为链表的阈值
    static final int UNTREEIFY_THRESHOLD = 6;

    // 当哈希表中的容量大于这个值时,表中的桶才能进行树形化
    // 否则桶内元素太多时会扩容,而不是树形化
    // 为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
    static final int MIN_TREEIFY_CAPACITY = 64;

	// HashMap的三大数据结构之一 数组。
    transient Node<K,V>[] table;

    // Entry 的set集合 遍历时使用
    transient Set<Map.Entry<K,V>> entrySet;

    // 已经存储的数据容量
    transient int size;

    // 被修改的次数
    transient int modCount;

	 /**
     * 扩容的阈值
     * 1、当 HashMap的size大于threshold时会执行resize操作。
     * 2、threshold=capacity*loadFactor
     */
    int threshold;

    // 负载因子参数
    final float loadFactor;

初始化方法,其中推荐的 第二种,当我们预先知道元素的个数时, 可以有效的避免扩容。


	// 无参初始化, 默认容量时16,负载因子 = DEFAULT_LOAD_FACTOR = 0.75f
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
	
	/**
     * 带参数的初始化。【推荐,可以避免扩容】
     *
     * 注意:HashMap推荐initialCapacit最好是2的n次幂,有利于均匀分布数组的下标。
     *      不过放心在tableSizeFor已经帮我们处理好了
     * @param initialCapacity 初始化的容量
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
	
	 /**
     * 带参数的初始化
     * @param initialCapacity 初始化的容量
     * @param loadFactor 负载因子
     */
    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;
        // 设置第一次需要扩容时的阈值,此时由 initialCapacity 决定的,和其他数据无关
        this.threshold = tableSizeFor(initialCapacity);
    }

	//  返回比给定目标 大的 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;
    }

重点 put方法:

我们初始化 HashMap 的时候,并没有创建数组,仅仅是对一些参数进行赋值:

  1. 在我们put 一个元素的时候 判断 table是否已经初始化,并将其初始化,是否达到扩容阈值,进行扩容。
  2. 计算 key 的hash值 并与数组的长度做:(n - 1) & hash 计算,计算key 在数组上的下标位置。
    a. 如果为空,直接加入
    b.如果为链表,放在最后,并判断是否进行 链表转化为 红黑树
    c.如果为红黑树,则放在红黑树里,并对红黑树进行修复
    d.如果在放入数组、链表、红黑树时,找到相同的 key 则将其原先的 Value 替代。
  3. put 方法源码:
	
	// put 键值对到Map中 【重要】
    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;

        // 判断储存数据的table 是否为空,并将其初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 通过(n - 1) & hash计算其在数组的下标位【目的可以使元素均匀的分布在数组上】
        // 如果当前数据位置没有元素,就放在这个位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // 当前key对应的节点
            Node<K,V> e;
            K k;
            // 我们保存的 key 与数组位置的key重复,最后面会把value值替代
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            /**
             * 当前节点是红黑树:
             * 1、如果红黑树中存在相同的 key 返回该节点
             * 2、如果红黑树中不存在相同的 key,将key-value生成新的节点,放入到红黑树中,返回null
             * 由于博主对红黑树理解不够透彻,暂时不去解读putTreeVal方法
             */
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            /**
             * 当前节点是链表
             * 1、遍历链表,链表中存在相同的 key 返回该节点
             * 2、链表中不存在相同的 key,将key-value生成新的节点,放入到链表最后,
             */
            else {
                // 遍历链表
                for (int binCount = 0; ; ++binCount) {
                    // 到了链表末端
                    if ((e = p.next) == null) {
                        // 生成新的节点,赋值到链表最后
                        p.next = newNode(hash, key, value, null);
                        // 判断是否将当前节点 链表转红黑树
                        // 条件1:链表的个数 >= TREEIFY_THRESHOLD - 1
                        // 条件2: 数组的大小 < MIN_TREEIFY_CAPACITY, 满足条件1,不满足条件2会进行扩容
                        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;
                }
            }
            // 表示在key存在于 HashMap中,将值进行更新
            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;
    }
	
	/**
     *初始化或加倍表大小。如果为空,则分配
     *与初始容量目标保持一致。
     *否则,因为我们使用的是二次展开的幂,所以
     *每个bin中的元素必须保持在同一索引中,或者移动
     *在新表中使用两个偏移量的幂。
     *
     *@return table
     */
    final Node<K,V>[] resize() {
        // 保存数据的 数组
        Node<K,V>[] oldTab = table;
        // 获取数据长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 旧的扩容阈值
        int oldThr = threshold;

        // 扩容后 数组长度
        int newCap = 0;
        // 扩容后 下一次在扩容的阈值
        int newThr = 0;
        // 原来数组已经初始化
        if (oldCap > 0) {
            // 容量已经达到最大,不进行处理了
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            // 将原来的数组容量(oldCap)扩大一倍,赋予新的数组容量(newCap)
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                // 将扩容阈值(oldThr)扩大一倍,赋予新的扩容阈值(newThr)
                newThr = oldThr << 1; // double threshold
        }
        // 初始容量设置为阈值
        else if (oldThr > 0)
            newCap = oldThr;
        else {
            // 初始化数组容量
            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;


        // 定义一个新的数组长度为 newCap;
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        // 数据迁移
        if (oldTab != null) {
            // 先遍历老的数组
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                // 拿到原先数组下标的数据
                if ((e = oldTab[j]) != null) {
                    // 位置置空,帮助GC
                    oldTab[j] = null;

                    // 当前位置元素 没有下一个节点
                    if (e.next == null)
                        // 根据当前元素的 hash 和新数组的大小,计算元素E在新的数组中的位置
                        // 为什么要 e.hash & (newCap - 1) 计算下标? 因为这样会使得元素在数组上的分布更加均匀
                        newTab[e.hash & (newCap - 1)] = e;
                    /**
                     * 先看下面链表的迁移方式,在看红黑树的迁移方式 【注意】
                     */
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    /**
                     * 链表的迁移方式
                     * 1、 & 运算规则:两个数都转为二进制,然后从高位开始比较,如果两个数都为1则为1,否则为0。
                     * 2、put方法计算时(oldCap - 1) & e.hash < oldCap,保证了结果始终在数组范围内。
                     * 3、扩容时:oldCap & e.hash = 0表示当前的key是属于原来数组范围内,oldCap & e.hash != 0 表示在扩容的范围内
                     * 4、根据上面的计算结果,定义低位和高位两个链表,最后复制到新的数组中newTab
                     * 5、这也是数组每次扩容两倍的原因
                     * 【这里需要仔细的琢磨琢磨!!!!】
                     */
                    else {
                        // 低位链表的头和尾节点
                        Node<K,V> loHead = null, loTail = null;
                        // 高位链表的头和尾节点
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        // 根据hash值 将当前节点下的链表分为高位和低位链表
                        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;
                        }
                        // 将高位链表赋值到(j + oldCap)位置,
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
	
	// 是否将链表转为红黑树
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        // 数组的大小 < MIN_TREEIFY_CAPACITY, 会进行扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        // 执行树化
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            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);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

put 方法中一些注释做了一些解释,最主要的还是在 resize() 扩容方法,在数组、链表、以及红黑树中查找位置的代码非常的重要,由于博主对红黑树研究不够深入,暂时没进行解析。

get方法:

看完 put 方法在看 get 方法就非常简单
1. 先计算在数组上的位置: (n - 1) & hash
2. 查看数组上节点key值是否相同
3. 然后判断 是链表 还是红黑树,进行遍历,如果没有找到相同的key 则返回 null


// 获取Values 值
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    // 根据 key的hash 和 key的值获取节点
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 根据 (n - 1) & hash 找到对应的下标位,是否有元素
        if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {
            // 数组下表位元素key 与 传入的key相同,直接返回该节点
            if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 查找first后的元素
            if ((e = first.next) != null) {
                // first 是红黑树根节点
                if (first instanceof TreeNode)
                    // 遍历红黑树返回节点
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // first 是链表
                do {
                    // 遍历链表返回节点
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

总结:

HashMap 首先需要了解其三种数据结构,再了解其扩容的原理,以及链表和红黑树之间的转换,基本上就OK 了;

Hashtable

Hashtable 其实是 HashMap 的前身,Hashtable是线程安全的,它的组成结构,只有数组和链表并在很多修改数据的方法上加入了 synchronized 来保证线程安全性:
put 和 get 代码如下:


public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }
        // 数组
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        // 计算下标位置
        int index = (hash & 0x7FFFFFFF) % tab.length;
        // 获取下标位置的元素
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        // 下标元素和其后面的链表是否与key重复,存在则将value替代,并返回
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
        // 添加一个节点
        addEntry(hash, key, value, index);
        return null;
    }
	
	// Get
    public synchronized V get(Object key) {
        // 数组
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        // 计算下标
        int index = (hash & 0x7FFFFFFF) % tab.length;
        // 查询当前的下标是否有包含key的数据 【当前下标位置的元素可能的值:null,节点,链表】
        for (Entry<?,?> e = tab[index]; e != null ; e = e.next) {
            if ((e.hash == hash) && e.key.equals(key)) {
                return (V)e.value;
            }
        }
        return null;
    }

	public synchronized int size() {
        return count;
    }
	
	public synchronized boolean isEmpty() {
        return count == 0;
    }
ConcurrentHashMap

相比 Hashtable,ConcurrentHashMap 采用了分段锁的概念,它的数据结构和HashMap基本相同,采用了 CAS 原子操作修改一些属性和元素避免并发问题。并 synchronized 对数组的每个下标位置元素进行加锁,保证当前节点桶【链表、红黑树】 并发安全,
我们来简单的看一下 put 的代码:


// put
    public V put(K key, V value) {
        return putVal(key, value, false);
    }


    final V putVal(K key, V value, boolean onlyIfAbsent) {
        // key、value 不能为空
        if (key == null || value == null) throw new NullPointerException();
        // 获得key的hash值
        int hash = spread(key.hashCode());
        // 用来计算在这个节点总共有多少个元素,用来控制扩容或者转移为树
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 数组为空,初始化数组
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // 根据 (n - 1) & hash 获取当前位置的节点
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // 通过CAS线程安全,将当前的节点放置到数组指定位置
                if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // ----------------------------
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                // 给当前的节点加锁,保证操作当前的节点线程安全
                synchronized (f) {
                    //  再次取出要存储的位置的元素,跟前面取出来的比较
                    if (tabAt(tab, i) == f) {
                        //  取出来的元素的hash值大于0,当转换为树之后,hash值为-2
                        if (fh >= 0) {
                            binCount = 1;
                            //遍历这个链表
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //  要存的元素的hash,key跟要存储的位置的节点的相同的时候,替换掉该节点的value即可
                                if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                                (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    //当使用putIfAbsent的时候,只有在这个key没有设置值得时候才设置
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                //如果不是同样的hash,同样的key的时候,则判断该节点的下一个节点是否为空,
                                if ((e = e.next) == null) {
                                    //为空的话把这个要加入的节点设置为当前节点的下一个节点
                                    pred.next = new Node<K,V>(hash, key,
                                            value, null);
                                    break;
                                }
                            }
                        }
                        //表示已经转化成红黑树类型了
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            //调用putTreeVal方法,将该元素添加到树中去
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                    value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    //当在同一个节点的数目达到8个的时候,则扩张数组或将给节点的数据转为tree
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //计数
        addCount(1L, binCount);
        return null;
    }
总结:

在学习 Map 的时候,建议先对 数组、链表、红黑树 有一定的了解,然后搞懂其数据结构,先对HashMap进行掌握与熟悉,HashTable 和 ConcurrentHashMap 可以看做是 HashMap 的变形,更好的去学习。源码还是要去一点点的阅读,了解其设计理念,则在以后的面试中会百战不殆。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值