Java基础-HashMap源码

Map 是一种键-值对(key-value)集合,Map 集合中的每一个元素都包含一个键对象和一个值对象。其中,键对象不允许重复,而值对象可以重复。并且值对象还可以是 Map 类型的,就像数组中的元素还可以是数组一样。但是Map不保证映射元素的顺序HashMap只允许存在一个为空的键,但可以存在多个为空的值

先来看一下源码中几个比较重要的成员变量

//初始化容量
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;

//当链表长度大于TREEIFY_THRESHOLD时,则转换为红黑树结构
//可以理解为链表的最大长度
static final int TREEIFY_THRESHOLD = 8;

//当红黑树长度小于UNTREEIFY_THRESHOLD时,转换为链表长度
//可以理解为红黑树的最小长度
static final int UNTREEIFY_THRESHOLD = 6;


-----------------------------------------------------------------------------------------------

//实际存储key和value的数组,这里将key和value封装成了Node对象。
transient Node<K,V>[] table;

//键值对(key-value)的总个数
transient int size;

//修改次数
transient int modCount;

//扩容的时机
//threshold表示当HashMap的size大于threshold时会执行resize进行扩容操作。 
//threshold=capacity*loadFactor
int threshold;

//加载因子的实际值
//加载因子用来衡量HashMap满的程度。loadFactor的默认值为0.75f。计算HashMap的实时加载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。
final float loadFactor;

构造函数

无参构造函数
//无参构造函数
public HashMap() {
    //赋值默认的加载因子 0.75F
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
参数为int类型
//当指定初始容量时,会使用初始容量和构造因子,调用另一个构造函数构造一个空的hashMap
public HashMap(int initialCapacity) {
     this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

//这个构造函数就是上边的this
//initialCapacity 是指定的初始化容量
//loadFactor 是指默认的加载因子
public HashMap(int initialCapacity, float loadFactor) {
    //判断初始化容量是否小于0
    if (initialCapacity < 0)
        //小于0则抛出异常
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    //判断初始化容量是否大于最大容量
    if (initialCapacity > MAXIMUM_CAPACITY)
        //是 则将容器初始化为最大容量
        initialCapacity = MAXIMUM_CAPACITY;
    //判断,当加载因子小于0或者加载因子为空时抛出异常。
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    //通过tableSizeFor方法计算出不小于指定容量的最小的2的幂的结果,赋值给成员变量threshold
    this.threshold = tableSizeFor(initialCapacity);
}

参数为Map集合
public HashMap(Map<? extends K, ? extends V> m) {
    //默认加载因子0.75f
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    //获取参数集合大小
    int s = m.size();
    //判断参数集合大小是否 > 0 
    if (s > 0) {
        //判断当前table数组是否为空(是否初始化)
        if (table == null) { // pre-size
            //根据参数集合大小计算出要创建的hashMap容量
            //计算方法:参数集合大小 / 加载因子(0.75) + 1 
            //若计算结果大于上限容量 则设置新集合的容量为上线容量,反之设置计算结果为新集合容量
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                threshold = tableSizeFor(t);
        }
        //若已经初始化,则判断参数集合大小,若大于threshold(扩容阈值),则先调用resize()方法进行扩容。
        else if (s > threshold)
            //扩容
            resize();
        //将key-value取出,利用key算出hash值再进行存入本地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);
        }
    }
}

//计算hash值
static final int hash(Object key) {
    int h;
    //首先判断key是否为空。(key == null)
    //若为空 则为 0
    //若不为空 则首先通过hashCode()方法计算key的hashCode值,然后赋值给h,最后带符号右移16位
    //int 32位 1111 1111 1111 1111 0000 0000 0000 0000
    // ^ 此符号表示异或 对象的hashCode的值的高位(前16位)
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

//异或运算的作用:降低hashCode的冲突率。使hashCode的值更加随机。

在了解方法之前,先看一下hashMap的数据结构图

put方法

public V put(K key, V value) {
    //根据key值获取hashCode值,调用putVal方法新增
    return putVal(hash(key), key, value, false, true);
}

//putVal
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是否为空或者长度是否为0
    if ((tab = table) == null || (n = tab.length) == 0)
        //初始化table数组获取长度。
        n = (tab = resize()).length;
    //首先将table数组长度-1,然后和传进来的key的hash值做"与(&)"运算,最后赋值给i
    //而变量i就是元素在数组中存储的位置
    //在将tab中下标为i的元素节点取出赋值给p
    //最后判断这个链表对象p是否为空
    if ((p = tab[i = (n - 1) & hash]) == null)
        //如果为空,则创建节点存放元素
        tab[i] = newNode(hash, key, value, null);
    //如果p 不为空
    else {
        Node<K,V> e; K k;
        //判断p的hash和key与传进来的hash和key是否相同
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //取出p节点的元素赋值给Node<K,V> e;
            e = p;
        //判断当前p的数据结构是不是树结构
        //jdk1.8的优化方案,推出了红黑树结构,提高性能。
        else if (p instanceof TreeNode)
            //基于红黑树的插入逻辑进行处理
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //如果不是红黑树结构,则以链表形式插入
        else {
            for (int binCount = 0; ; ++binCount) {
                //判断p的下一个元素是否为空
                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;
                }
                //如果当前链表包含要插入的值,结束循环。
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //将e节点对象赋值给与运算出的p元素节点
                p = e;
            }
        }
        //判断如果e不等于空
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //修改次数+1
    ++modCount;
    //判断当前数组大小是否大于预值
    if (++size > threshold)
        //大于预值,则扩容
        resize();
    afterNodeInsertion(evict);
    return null;
}

get方法

相对于put方法而言,get方法相对比较简单

public V get(Object key) {
    Node<K,V> e;
    //使用key获取对应的hash值  hash(key)
    //获取对应的元素节点  getNode(hash(key), key)
    //三目运算符 e == null ? null : e.value;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

get方法首先通过key值获取对应的hash值,再利用getNode方法获取对应元素节点,若为空则返回null,不为空则返回节点中对应的value值。

tableSizeFor

/**
  * Returns a power of two size for the given target capacity.
  */
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;
}

tableSizeFor方法的作用就是返回大于等于cap的最小的2的倍数。(如果cap是2的倍数,那么返回值就是cap)

方法分析

首先,为什么要对cap做减1操作。int n = cap - 1;
这是为了防止,cap已经是2的幂。如果cap已经是2的幂, 又没有执行这个减1操作,则执行完后面的几条无符号右移操作之后,返回的capacity将是这个cap的2倍。

如果n这时为0了(经过了cap-1之后),则经过后面的几次无符号右移依然是0,最后返回的capacity是1(最后有个n+1的操作)。
这里只讨论n不等于0的情况。

由于n不等于0 ,所以转化为二进制后肯定有一个bit上的数是1,这是考虑最高位的1,通过无符号右移1位后再做或运算,(或运算:有一个1则为1,否则为0),那么这是就是000011xxxxxxx ,如下图:

在这里插入图片描述

这时进入第二次位移,将或运算后的结果再右移2,假设此时n等于0000 1101 那么位移2位后是 00000011 ,再做或运算后就是 0000 1111。

以此类推,需要注意的是,**容量最大也就是32bit的正数,因此最后n |= n >>> 16; ,最多也就32个1,但是这时已经大于了MAXIMUM_CAPACITY ,所以取值到MAXIMUM_CAPACITY 。 **

Hash方法

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

三目运算符比较简单就不讲了

主要看这一部分(h = key.hashCode()) ^ (h >>> 16);

从上面的代码可以看到key的hash值的计算方法。key的hash值高16位不变,低16位与高16位异或^作为key的最终hash值。(h >>> 16,表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位不变。) 如下图:

为什么要这么干呢?
这个与HashMap中table下标的计算有关。

n = table.length;
index = (n-1) & hash;

因为,table的长度都是2的幂,因此index仅与hash值的低n位有关,hash值的高位都被与操作置为0了。

假设table.length=2^4=16。

由上图可以看到,只有hash值的低4位参与了运算。
这样做很容易产生碰撞。设计者权衡了速度、效用和质量,将高16位与低16位异或来减少这种影响。设计者考虑到现在的hashCode分布的已经很不错了,而且当发生较大碰撞时也用树形存储降低了冲突。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。

内部类Node

内部类Node是HashMap中不可或缺的一份子。我们知道HashMap源码结构是以数组+链表+红黑树组成。而Node就是HashMap中的链表对象类。

//内部类Node
static class Node<K,V> implements Map.Entry<K,V> {
    //hash值
    final int hash;
    //key
    final K key;
    //value
    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;
    }
	
    //获取key
    public final K getKey()        { return key; }
    //获取value
    public final V getValue()      { return value; }
    //toString
    public final String toString() { return key + "=" + value; }

    //hashCode
    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }
	
    //setValue
    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }
	
    //equals
    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;
    }
}

扩容机制

final Node<K,V>[] resize() {
    //获取当前数组(旧数组)
    Node<K,V>[] oldTab = table;
    //获取旧数组长度,若数组为空长度为0。
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //旧数组的扩容阈值(扩容时机)
    int oldThr = threshold;
    //扩容后的变量(新数组长度,扩容阈值)
    int newCap, newThr = 0;
    //如果旧数组长度大于0
    if (oldCap > 0) {
        //如果旧数组长度大于等于最大容量上限
        if (oldCap >= MAXIMUM_CAPACITY) {
            //则设置扩容阈值(扩容时机)为int类型的最大值
            threshold = Integer.MAX_VALUE;
            //返回
            return oldTab;
        }
        //如果不等于最大容量上限
        //1、将旧的容量左移1位(旧的容量*2) 赋值给新的容量
        //2、判断新数组的容量是否小于容量上限值(MAXIMUM_CAPACITY)并且大于等于容量初始值(DEFAULT_INITIAL_CAPACITY)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            //将旧的扩容阈值左移1位(*2)赋值给新的扩容阈值
            newThr = oldThr << 1; // double threshold
    }
    //如果旧数组长度小于或者等于0
    //判断旧的扩容阈值是否大于0
    else if (oldThr > 0) // initial capacity was placed in threshold
        //如果旧的扩容阈值大于0
        //将旧的扩容阈值赋值给新的扩容阈值变量
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        //如过旧的扩容阈值小于等于0
        //将新的容量设置为初始化容量 -- 注意这里是容量不是扩容阈值
        newCap = DEFAULT_INITIAL_CAPACITY;
        //计算出新的扩容阈值赋值给newThr
        //计算方法:初始化容量*加载因子
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //如果(计算后的)扩容阈值等于0
    if (newThr == 0) {
        //将新的容量*旧数组的加载因子的值赋值给ft
        float ft = (float)newCap * loadFactor;
        //1、判断容量小于容量上限值 并且 判断计算后的ft小于容量上限值
        //若同时为true 设置新的扩容阈值为计算后的ft
        //否则 设置新的扩容阈值为int类型的最大值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    //将当前的扩容阈值设置为新的扩容阈值
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //new一个新的数组,长度为新的容量值(newCap)
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //设置当前数组为新的数组
    table = newTab;
    //如果旧数组不为空
    if (oldTab != null) {
        //for循环
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //循环判断下标为j的位置是否"不为空"
            if ((e = oldTab[j]) != null) {
                //清空当前位置的元素
                oldTab[j] = null;
                //判断当前位置的下一个元素节点是否为空
                if (e.next == null)
                    //重新计算位置,进行元素保存
                    //新元素的位置 : 使用e.hash & 新元素扩容阈值-1 
                    newTab[e.hash & (newCap - 1)] = e;
                //如果当前位置的下一个元素节点不为空
                //判断是否是树结构
                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;
                        //根据e.hash & oldCap == 0 的规则划分为两个不同的链表
                        //二进制最高位 == 0 这是索引不变的链表元素
                        if ((e.hash & oldCap) == 0) {
                            //如果旧元素的尾部 "为空"
                            if (loTail == null)
                                //设置旧元素的头部为当前元素节点
                                loHead = e;
                            //如果旧元素的尾部 "不为空"
                            else
                                //设置旧元素的下一个元素为当前元素节点
                                loTail.next = e;
                            //设置e为旧元素
                            loTail = e;
                        }
                        //二进制最高位 == 1 这是索引发生改变的链表元素
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    //将索引不变的链表存入桶中(newTab)
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead; //存入j的位置
                    }
                    //将索引改变的链表存入桶中(newTab)
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead; //存入j+oldCap的位置
                    }
                }
            }
        }
    }
    return newTab;
}

经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。


元素在重新计算hash之后,因为n是原来的两倍,那么n-1的范围在高位多出1bit(红色)。因此新的下标就会发生这样的变化:

因此,在扩展HashMap的时候,只需要判断新增的bit是1还是0就可以。若是0则原索引地址不变,若是1 则是原索引地址+旧扩展阈值(oldCap) 。下面是16扩充为32的示意图。

文章参考:
Java 8系列之重新认识HashMap
HashMap方法hash()、tableSizeFor()

推荐阅读:
快速理解二进制计数的基数和位权
“与”、“非”、“或”、“异或” 详解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值