HashMap源码理解

Posted 尹凯文

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashMap源码理解相关的知识,希望对你有一定的参考价值。

导语

HashMap是常用的数据结构,了解HashMap,对提高代码的效率有很大的帮助。HashMap在JDK1.8中对数据结构进行了优化:提高了查询和删除的效率。当然,这也导致了结构更加的复杂;但通过认真阅读源码,还是可以掌握其要领的。

读完本篇文章,你应该理解的内容

点击这里查看大图
点击这里查看大图

说明:HashMap的数据结构是个Hash表(可以理解为数组),每个槽中存放着一些节点。

  1. 一般情况下,一个槽中存放一个节点;
  2. 数据量较大时,一个槽中可能存放多个节点,此时,各个节点以链表的方式连接在一起;
  3. 当一个槽中的节点数很多时(8个以上),会以红黑树的方式来保存这些节点

源码理解

成员变量

//数组默认的大小
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//数组的最大容量
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;

//数组,真正用来保存数据的容器
transient Node<K,V>[] table;

//用于遍历,本篇不做介绍
transient Set<Map.Entry<K,V>> entrySet;

//大小
transient int size;

//修改的次数
transient int modCount;

//阈值:当数组中的数据的个数大于该值时,数组会扩充
int threshold;

//加载因子
final float loadFactor;

说明:从table中可以看出,HashMap最基本的数据结构是个数组;其余的成员变量单独分析是得不到什么结果的,需要结合下面的内容来理解。从常用到的put(),get(),remove()开始理解。

构造方法

在此之前,当然要看看它的构造方法是怎样的:

public HashMap() {
    //加载因子为默认值
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
    //这里并没有初始化数组
}

//自定义initialCapacity,加载因子使用默认值
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

//自定义initialCapacity,和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;

    //设置阈值;阈值大小为2的次方
    //例如:initialCapacity = 17 ,阈值为 32
    //      initialCapacity = 5 ,阈值为 8
    //      initialCapacity = 55 ,阈值为 64
    this.threshold = tableSizeFor(initialCapacity);
}

public HashMap(Map<? extends K, ? extends V> m) {
    //加载因子为默认值
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    //将m中的数据存到当前的Map中
    putMapEntries(m, false);
}

说明:前三个构造方法中,只是初始化了一些参数,没有过多的操作;第四个构造方法比较复杂,本篇读完后,再去看源码就容易理解了,这里不做讨论。

put()方法

//间接键值对
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) {
    //  tab --数组,用来保存数据的容器
    //  p   --i所对应数组槽中的第一个节点
    //  n   --数组的大小
    //  i   --当前键值对应该存储在数组中的位置
    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 {
        //  e--  用来标记符合条件的节点    
        //  k--  键
        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);
                    //如果链表的长度大于阈值(8),那么将链表转换成红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }

                //说明当前e符合条件,结束遍历
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }

        //当有节点符合要求,更新节点中数据
        if (e != null) { 
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;

            //LinkedHashMap中会用到,这里没处理
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //如果当前的大小大于阈值,扩充数组的大小
    if (++size > threshold)
        resize();

    //LinkedHashMap中会用到,这里没处理
    afterNodeInsertion(evict);

    return null;
}

说明:实现的细节非常繁琐,但是总结起来就很简单了:

  1. 没有相应的节点,就创建节点,并放到合适的位置
  2. 有相应的节点找到对应的节点,更新其中的数据

额外说明:

  1. put()不会重复保存key,HashSet就是利用了这点来实现去重的
  2. LinkedHashMap会重写其中的一些方法来实现相应的特性

get()方法

//根据key找到value
public V get(Object key) {
    Node<K,V> e;
    //找到相应的节点,返回value
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

//找到对应的节点
final Node<K,V> getNode(int hash, Object key) {
    //  tab     --  数组
    //  first   --  数组对应槽中的第一个节点
    //  e       --  对应的节点
    //  n       --  数组的长度
    //  k       --  键
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {

        //最上面那张图有个提醒:一般情况下,一个槽中只有一个数据,所以
        //一般情况下先检查第一个节点是否符合要求,符合,直接返回该节点,否则继续查找
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;

        //第一个节点不符合的处理
        if ((e = first.next) != null) {
            //数据结构为红黑树的处理
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);

            //数据结构为链表的处理
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    //没有找到对应的节点
    return null;
}

说明:get()可以分为这么几个步骤:

  1. 锁定槽
  2. 从槽中查找相应的节点
  3. 返回合适的数据

remove()方法

//移除相应的节点
public V remove(Object key) {
    Node<K,V> e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

//移除相应的节点
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    //  tab --  数组,用来保存数据的容器
    //  p   --  index所对应数组槽中的第一个节点
    //  n   --  数组的大小
    //index --  当前键应该存储在数组中的位置
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        //node  --  符合要求的节点
        //  e   --  标记当前节点的下一个节点
        //  k   --  key
        //  v   --  value
        Node<K,V> node = null, e; K k; V v;

        //最上面那张图有个提醒:一般情况下,一个槽中只有一个数据,所以
        //一般情况下先检查第一个节点是否符合要求,符合,直接返回该节点,否则继续查找
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;

        //第一个节点不符合的处理
        else if ((e = p.next) != null) {
            //数据结构为红黑树的处理
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            //数据结构为链表的处理
            else {
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }

        //判断node是否符合删除的条件
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            //结构为红黑树时的操作
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);

            //要删除的节点是链表且为槽中的首个节点的处理
            else if (node == p)
                tab[index] = node.next;

            //数据结构为链表的处理
            else
                p.next = node.next;
            ++modCount;
            --size;

            //LinkedHashMap中会用到,这里没处理
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

说明:简单来说就是:找到相应的节点并删除并且按照规则移动槽中剩余的节点。

结语

  1. 这时再去看第四个构造方法,无非就是变量传进来map,将数据封装到HashMap中来。
  2. 本文对链表以及红黑树的的操作没有做进一步的分析。个人认为,阅读源码,如果过分的关注细节可能会难以把握整体的思路;当然,有些时候看源码需要关注细节,这之间需要我们进行平衡,源码看多了,这种平衡感就会有的。(链表和红黑树的操作之后的文章会单独做一些说明)
  3. 最后,再一次将核心部分,也就是最开始的那张图贴一下。

点击这里查看大图
点击这里查看大图

转载请标明出处http://blog.csdn.net/qq_26411333/article/details/51723828

以上是关于HashMap源码理解的主要内容,如果未能解决你的问题,请参考以下文章

HashMap源码深入研究

[笔记]为啥hashmap查询速度快? 如何理解hashmap的散列?

HashMap详细解释+全站最硬核手撕源码分析

HashMap详细解释+全站最硬核手撕源码分析

HashMap部分源码剖析

面试常问的HashMap源码分析(jdk1.8)