JAVA集合源码解析 HashMap探索(基于JDK1.8)

JDK1.8HashMap探索

本文基于JDK1.8版本进行

国际惯例先来个大纲,以下就是按照大纲形式进行分析

1. 简介

hashmap结构
HashMap采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。红黑树是JDK1.8版本加入进来的,1.8之前HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的节点都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。

2.1类关系

HashMap

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

从关系图中我们知道:
HashMap继承了AbstractMap接口,能够实现其中的所有可选的Map操作
HashMap实现了Map接口,能够实现其中的所有可选的Map操作;
HashMap实现了Cloneable接口,能够使用clone()方法;
HashMap实现了Serializable接口,支持序列化操作

眼尖的朋友可以会发现,为什么继承了AbstractMap接口又要实现Map接口呢?
其实跟据java集合框架的创始人Josh Bloch描述:

Josh Bloch承认这是一个失误,最开始写java集合框架的时候,他认为这样写,在某些地方可能是有价值的,直到他意识到错了。显然的,JDK的维护者,后来不认为这个小小的失误值得去修改。所以就这样存在下来了。

2.2属性

    /**
     * 默认初始容量 - 必须是2的幂
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 为16,其实就是1 * 2的4次方

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

    /**
     * 没有在构造函数中指定时使用的加载因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 使用树而不是列表的容器计数阈值。
     * 将元素添加到至少包含多个节点的元素时,元素将转换为树。
     * 该值必须大于2,并且应该至少为8,以便与收缩时转换回普通箱的树木移除假设相关联。
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * 用于在调整大小操作期间对(拆分)桶进行的桶数阈值。
     * 应该小于TREEIFY_THRESHOLD,并且最多6个与在去除下的收缩检测吻合。
     */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
     * 桶中可能被树化的最小容量。
     * (否则,如果bin中的节点太多,则调整表的大小。)应该至少为4 * TREEIFY_THRESHOLD以避免调整大小和树化阈值之间的冲突。
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

2.3 构造函数

这里写图片描述

HashMap(int initialCapacity, float loadFactor)构造函数

    /**
     * 用指定的初始容量和加载因子的构造函数。
     *
     * @param  初始容量
     * @param  加载因子
     */
    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))//如果加载因子小于等于0或者未确定
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

tableSizeFor(int cap)是返回大于等于cap的最小的二次幂数值

    /**
     * 返回大于等于cap的最小的二次幂数值。
     */
    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;
    }

HashMap(int initialCapacity)带一个参数的构造函数

    /**
     * 使用指定的初始容量和默认加载因子(0.75)的构造函数
     * @param  初始容量
     *
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

无参构造函数HashMap()

    /**
     * 使用默认初始容量(16)和默认加载因子(0.75)的构造函数
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

带Map参数的构造函数

    /**
     * 带Map参数的构造函数
     *
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

    /**
     * 实现Map.putAll和Map构造函数
     */
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();//保存m的大小
        if (s > 0) {
            //如果table为空
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)//如果大于临界值
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)//table不为空并且s的值大于临界值
                resize();//扩容
            //将m的元素添加到hashmap集合中
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

putVal(hash(key), key, value, false, evict)是将元素添加到HashMap集合中,具体分析在下面2.4中可见

2.4核心方法

因为hash方法在很多方法中都用到用来寻找对象键的位置,所以我们需要了解

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 右移16位,忽略符号位,空位都以0补齐    // 如value >>> num -- num 指定要移位值value 移动的位数。
    }

hash方法会先获取对象的hashCode()值,然后在异或上右移16位后的hashCode()值做运算。

(1)put方法

    /**
     * 将指定的值与此映射中指定的键关联。 如果集合之前包含相匹配的映射,则旧值将被替换。
     *
     * @param key
     * @param value
     */
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

会调用putVal()方法

    /**
     * 实现Map.put的核心方法
     *
     * @param hash hash处理过的值
     * @param key 键
     * @param value 值
     * @param onlyIfAbsent true,不更改现有值
     * @param evict false,处于创建模式。
     */
    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)//如果table初始化数组结点为空或者长度为0
            n = (tab = resize()).length;//进行扩容操作
        if ((p = tab[i = (n - 1) & hash]) == null)//确定桶中 (n - 1) & hash 位置的元素是否为空
            tab[i] = newNode(hash, key, value, null);//新生成结点放入桶中
        else {//桶中已存在元素
            Node<K,V> e; K k;
            if (p.hash == hash &&//比较数组中第一个值的hash值和key是否相等
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;//将值赋值给e保存
            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) // 链表长度>于临界值
                            treeifyBin(tab, hash);//桶的树形化处理
                        break;//跳出循环
                    }
                    if (e.hash == hash &&//判断链表中结点的key值与插入的元素的key值是否相等
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;//相等,跳出循环
                    // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
                    p = e;
                }
            }
            if (e != null) { // 存在匹配的结点
                V oldValue = e.value;//保存e的结点值
                if (!onlyIfAbsent || oldValue == null)//如果onlyIfAbsent为false或者旧值为空
                    e.value = value;//保存新值
                afterNodeAccess(e);//访问后回调
                return oldValue;//返回旧值
            }
        }
        //结构性加1
        ++modCount;
        if (++size > threshold)//如果实际大小大于临界值
            resize();//扩容
        afterNodeInsertion(evict);//插入后回调
        return null;
    }

1)先判断table == null或length == 0,如果满足则进行resize()扩容操作。
2)根据hash方法判断桶中 (n - 1) & hash 的位置是否为空,是的话就直接插入。
3)如果桶中 (n - 1) & hash 的位置不为空,比较数组中第一个值的hash值和key是否相等,相等则直接覆盖,
4)如果桶中 (n - 1) & hash 的位置不为空,且数组中第一个值的hash值和key不相等,则判断是否为红黑树结构,是则把键值对插入红黑树中。
5)如果桶中 (n - 1) & hash 的位置不为空,且数组中第一个值的hash值和key不相等,不为红黑树结构,则一定为链表结构,遍历链表且在尾部加入新结点,同时判断链表长度是否大于8,大于就执行treeifyBin(tab, hash)方法,转化为红黑树提高效率。如果链表中存在的结点值与插入的元素的key值相等,则直接覆盖。
6)插入成功后如果实际大小大于临界值threshold,进行扩容操作
7)扩容方法:每次扩容大小为原来的2倍,并且桶中的元素位置不变或者偏移到原来2倍的位置

    /**
     * 初始化或拓展数组大小。
     * 如果为空,则根据阈值中保存的初始容量目标进行分配。
     * 否则,使用二次幂次扩展,每个桶的元素必须保持相同的索引,或者在新表中以两个偏移量的幂移动。
     *
     */
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;//保存table数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//保存table的容量
        int oldThr = threshold;//保存临界值
        int newCap, newThr = 0;//初始化新的数组结点大小,临界值
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {//如果table数组大小大于最大容量
                threshold = Integer.MAX_VALUE;//重新赋值临界值
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)//如果旧容量翻倍后小于最大容量,并且旧容量>=默认初始容量
                newThr = oldThr << 1; // 临界值扩容为2倍
        }
        else if (oldThr > 0) //如果之前临界值>0
            newCap = oldThr;//临界值赋值给新数组初始化容量
        else {
            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"})
            //初始化newTab数组
            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) {//遍历判断如果结点不为空
                    oldTab[j] = null;//置空结点
                    if (e.next == null)//如果结点后继为空,也就是只有一个结点
                        newTab[e.hash & (newCap - 1)] = e;//将e放入newTab中e.hash & (newCap - 1)的位置
                    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) {//如果hash值桉位与上旧容量值为0
                                if (loTail == null)//如果低位结点为0
                                    loHead = e;//赋值低位头结点
                                else
                                    loTail.next = e;//赋值后继
                                loTail = e;//赋值低位结点
                            }
                            else {//如果hash值桉位与上旧容量值不为0
                                if (hiTail == null)//如果hi低位结点为空
                                    hiHead = e;//赋值hi头结点
                                else
                                    hiTail.next = e;//赋值hi低位后继
                                hiTail = e;//赋值hi低位结点
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {//如果lo低位不为空
                            loTail.next = null;//置空后继
                            newTab[j] = loHead;//赋值新结点值
                        }
                        if (hiTail != null) {//如果hi低位不为空
                            hiTail.next = null;//置空后继
                            newTab[j + oldCap] = hiHead;//赋值新结点值
                        }
                    }
                }
            }
        }
        return newTab;
    }

8)比较重要的是桶的树形化操作,需要满足数组的大小大于64时,才会转化为红黑树,否则会进行扩容操作。

    /**
     *  用索引替换桶中的所有链接节点,除非表太小,。
     */
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)//如果tab为空或者tab数组的长度小于被树化的最小容量64
            resize();//扩容
        else if ((e = tab[index = (n - 1) & hash]) != null) {//如果根据hash寻址的项不为空(e 为链表中的第一个结点)
            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);
        }
    }

(2)get(Object key)方法

    /**
     * 返回指定键映射到的值,
     * 如果不包含返回null。
     */
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

其实里面调用了getNode(hash(key), key)方法

    /**
     * 实现Map.get和相关方法
     *
     */
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&//tab不为空并且长度大于0并且根据hash寻址的项也不为空
            (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;
    }

getNode的时候如果2个键的hashcode相同了,则会判断key是否相等,相等返回值,否则继续在链表或者红黑树中查找对应值

(3)remove(Object key)方法

    /**
     * 如果存在,则从该映射中删除指定键的映射。
     *
     */
    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

里面调用了removeNode方法

    /**
     * 实现Map.remove和相关的方法
     * @param hash hash for key
     * @param key the key
     * @param value value匹配如果matchValue,否则忽略
     * @param matchValue 如果为true,则仅在值相等时删除
     * @param movable 如果在移除时不移动其他节点
     * @return the node, or null if none
     */
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&//如果tab不为空,长度大于0,并且根据hash寻址的项也不为空
            (p = tab[index = (n - 1) & hash]) != null) {
            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);
                }
            }
            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;//用链表的方式删除结点
                //结构性加1
                ++modCount;
                //大小减1
                --size;
                //删除后回调
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

3.思考问题

  • 为什么加载因子是0.75?
    这个是在时间和空间成本上寻求一种折中。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少rehash操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生rehash 操作。
  • HashMap中如何解决Hash碰撞?
    链地址法
  • 如果两个键的hashcode相同,如何获取值对象?
    它们会储存在同一个桶位置的链表中。键对象的equals()方法用来找到键值对。
  • 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?
    会进行扩容操作
  • 为什么String, Interger这样的wrapper类适合作为键?
    因为他们是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了,放入和取出时的hashCode()一样,就能方便的从HashMap取值。

当然还有很多的问题,暂时就想到这些,有其他的欢迎大家交流补充。

4.总结

HashMap的工作原理
HashMap基于hashing原理,通过put()和get()方法储存和获取对象。
当获取对象时,很关键的通过equals()来找到对应的键值对,避免了不同键产生相同hashcode的情况,当我们将键值对传递给put方法时,它会调用KEY的hashCode()方法计算hashcode,然后找到桶的位置来存储对象。HashMap采用链地址法解决碰撞问题,如果发生碰撞,就存储该对象在链表的下一节点处。
注意,HashMap实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,
如:
Map m = Collections.synchronizedMap(new HashMap(…));

猜你喜欢

转载自blog.csdn.net/ouzhuangzhuang/article/details/80177513