JDK1.8源码:HashMap解读

HashMap是我们最常用的集合类型之一了。也由于其高效、使用方便,我们有必要对其内部进行梳理一番。

JDK1.8源码中,关于Map的类图关系如下:

Map类图关系.png

Map家族的对比

Map的类图关系中,我们可以看出还是蛮丰富的。需要用到顺序的,可以使用TreeMap,需要线程安全的可以使用HashTableConcurrentHashMap。其各自特点总结如下:

特点
HashMap存储数据无序;非线程安全;key可为,但是其桶索引为0;效率为O(1)
HashTable远古时代就具有的,存储数据无序;线程安全,但是是使用笨重的Synchronizedkey不可为
ConcurrentHashMap新贵,存储数据无序;线程安全,采用CASkey不可为
TreeMap存储数据有序;非线程安全
LinkedHashMap存储数据有序;非线程安全

让我们先从HashMap开始。

HashMap概述

HashMap存储样式

JDK1.7中,HashMap由数组和链表构成,当链表数据特别多的时候,很明显的其效率受到影响。于是,在JDK1.8中的HashMap当链表数据过长时,转为红黑树的数据结构。

HashMap源码

我们选取常用的几个方法(实例化、put)来看下源码。

HashMap属性

    //Map默认初始化的容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

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

    //对Map的entrySet结果缓存
    transient Set<Map.Entry<K,V>> entrySet;

    //key-value对的数量
    transient int size;

    //增加或者删除Map元素、rehash的次数
    transient int modCount;

    //HashMap需要resize的阈值
    int threshold;

    //负载因子
    final float loadFactor;

HashMap初始化

选取个复杂点的构造方法:

    //initialCapacity表示HashMap的容量,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;
        //tableSizeFor是将initialCapacity转为不小于其的最小2的幂次方
        this.threshold = tableSizeFor(initialCapacity);
    }
    
    //tableSizeFor的方法如下
    //目前算法我看的不太明白
    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;
    }

大家通过源代码可以看到,初始化时,可以说几乎没发生没什么事情。只赋值了loadFactor和确保Map的容量为2的幂次方。

这里就有个问题?为什么需要确保Map的容量为2的幂次方?其实这是个非常规的设计,常规的设计是把桶的大小设计为素数(参考:https://blog.csdn.net/liuqiyao_01/article/details/14475159)。在讲完put方法后我们来阐述。

其实,这里可以看作是Map的延迟初始化。在首次put元素时,会初始化属性table。在这里顺便提下table的类型Node。在为链表时,其数据结构为:

//链表节点
static class Node<K,V> implements Map.Entry<K,V> {
        //key的hash值
        final int hash;
        //Map的key值
        final K key;
        //Map的key对应的value
        V value;
        //下一个元素
        Node<K,V> next;

        //省略getter&setter、equals
    }

当转为红黑树时,其结构为:

//树节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
}

HashMap put方法

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //1.首次存入元素时,会在resize方法中初始化table
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //2.如果key对应的索引位置((n - 1) & hash)没有元素,则直接存入元素
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {//3.如果key对应的索引位置有元素
            Node<K,V> e; K k;
            //3.1 如果key相同,则直接覆盖key对应的value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //3.2 如果key不同,判断Node是否属于红黑树类型,是则入树
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {//3.3 Node属于链表节点类型
                for (int binCount = 0; ; ++binCount) {
                    //3.3.1 下一个节点为空,则插入链表中
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //3.3.1.1 节点数量是否超过树化的阈值,超过则进行树化。注意此处并非一定会转化为红黑树,还会判断属性table的长度,可以参考treeifyBin方法
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifyBin(tab, hash);
                        break;
                    }
                    
                    //3.3.2 下一个节点不为空,判断key是否相同,相同则覆盖旧值
                    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;
                //为LinkedHashMap预备
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //添加修改次数
        ++modCount;
        //超过阈值则分配空间,重新rehash运算
        if (++size > threshold)
            resize();
        //为LinkedHashMap预备
        afterNodeInsertion(evict);
        return null;
    }

我们可以画个流程图:

HashMap put

其他的方法,譬如getremove等,主要的都是先要确定key对应的索引。即hash & (n - 1)。其中hashkey对应的hash值,ncapacity,即Map的容量。
我们先来看下hash的计算:

    static final int hash(Object key) {
        int h;
        //如果为null,则索引为0;否则是其hashCode低16位与hashCode高16位的抑或结果
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

为什么hashCode需要与其高16为抑或?简单点说就是让高位和低位都参与运算,使key的分布尽量更均匀些。参见https://zhuanlan.zhihu.com/p/21673805

有了hash值之后,我们可以计算索引的位置了。一般都是取模运算,但是大家都知道计算机是二进制的,位运算会比取余快多了。所以这里的hash & (n - 1)可以看做是用空间来换取时间。因为当n为2的幂次方时,n-1的二进制恰恰全是1,其与hashCode的二进制与值正好是取模的结果。这里就回答了上面为什么需要确保Map的容量为2的幂次方的问题。

HashMap的源码目前就分析到这里。流程其实说起来不是很难,难的关键就是为什么设计者会是这样的设计?这恰恰是我们需要多思考的。本篇在关于这一点上还是远远不足的,大家可以多搜索几篇来看看,多思考思考。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值