HashMap源码

相信大家在面试的过程中,经常会被问到一个这样的问题:“你了解hashmap的底层原理吗?”,大多数初级人员或许只是了解它的底层数据结构是什么,基本的作用是什么,但是一旦问到扩容过程,put的过程,红黑树的变色和翻转(本篇不支持),大家难免就无法从容面对。

那我们就一块去看下hashmap的底层源码是什么样子的~~

开始之前请大家思考一下,我们常见的数据结构有哪些?典型的代表又有哪些呢?

01
常见数据机构

我们比较熟悉的应该是这几种:数组,链表(单向和双向),树形,图形

典型的代表:

数组:类似如下,典型代表是Arraylist和Vector

链表:典型代表是LinkedList

双向:

单向

红黑树:典型代表是hashmap(jdk1.8之前是数组加链表,1.8之后又增加了红黑树)

hashmap的结构组成是 :数组+链表+红黑树

02 HashMap的常见参数

我们可以先考虑这么几个问题:

既然hashmap的底层结构含有数组,那么我们应该知道,数组是要指定一个大小的,那么默认大小是多少呢?

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

DEFAULT_INITIAL_CAPACITY参数就是默认的大小值,就是16

既然有默认大小,那么长度有没有上限呢?

/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

MAXIMUM_CAPACITY就是数组的上限值

默认长度是16,这个长度是有时满足不了我们的业务需求的,这也就意味着我们要扩容,很多人可能觉得只要数组长度达到16就开始扩容,俗话说未雨绸缪,既然知道存在长度可能不够的情况,那我们就要提前做准备才是!

/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

DEFAULT_LOAD_FACTOR 加载因子,也就是说当容器使用了16*0.75=12,的时候,就开始扩容。

jdk8引入了红黑树,原因自然是因为红黑树拥有更高的效率,但是并没有抛弃掉链表。为什么呢?任何东西,存在即合理,红黑树在一定程度上比链表更高效,但是有时候链表更有优势!

这也就意味着它们之间可以进行转换,根据不同的场景选择不同的存储结构,,这才是合理的,那么什么时候才会转换呢?

链表转红黑树:

/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2 and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 */
static final int TREEIFY_THRESHOLD = 8;

根据英文的意思就可以看出这是用于红黑树的,意味着当链表的长度>=8的时候,链表开始转换为红黑树。

红黑树转链表:

/**
 * The bin count threshold for untreeifying a (split) bin during a
 * resize operation. Should be less than TREEIFY_THRESHOLD, and at
 * most 6 to mesh with shrinkage detection under removal.
 */
static final int UNTREEIFY_THRESHOLD = 6;

UNTREEIFY_THRESHOLD 红黑树转链表,即树的深度<=6的时候,会转化为链表。

转换就意味着会产生冲突,为了避免冲突,我们还需要能够成为树的最小数量

/**
 * The smallest table capacity for which bins may be treeified.
 * (Otherwise the table is resized if too many nodes in a bin.)
 * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
 * between resizing and treeification thresholds.
 */
static final int MIN_TREEIFY_CAPACITY = 64;

MIN_TREEIFY_CAPACITY,是树的节点最小数量,依据就是 4 * 数组长度(16)=64

03put的过程

明白了上面几个参数的意义就是,我们就开始尝试去看一下源码,我们就以put方法为例子,看看到底是怎么个意思。

我们看到 putVal(hash(key), key, value, false, true);

这有4个参数,前两个分别是key(key的hash值),value,第三个代表遇到重复值是否要覆盖,fasle是覆盖,最后一个参数是指插入结束后要不要创建新的模式,false代表是(可参考英文注解)

此时你或许会疑问,为什么要对key进行hash化,这涉及到hash算法,大家想一下,map的数组长度默认是16,而且map是无序的,也就意味着要在下标为0-15中随机产生而且人家更要考虑的均匀性,就是0-15不仅要随机,还要每个数字都要雨露均沾。目的就是尽量让这些数字产生的更加公平均匀。

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

至于如何雨露均沾,往后看。

下面我会摘取源码的一块块的进行截取解读,建议大家比着源码来看

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)
        n = (tab = resize()).length;
看一下前几行的代码,首先定义了两个数组tab和p,以及两个int变量,n和i,我们继续往下看就知道作用了。

if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;

table是常量,记录了map的数据,我们每插入一条数据,table就会多一条记录下来。

这块代码就是初始化,看看是不是第一次添加数据,如果是就通过resize()方法给tab初始化,并由变量n记录当前长度,此时我们已经看到了两个变量的作用。

Resize()方法的作用有2个,初始化数组和扩容,最后我们会一起看一下,此处先认识有这样一个操作 。

if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
else {

数组下标值是 (n - 1) & hash 这行代码来获取的,为什么能实现雨露均沾,这个就涉及二进制的&运算,具体请关注往期文章

else后面的代码我并没有粘过来,为什么?因为这就涉及到一个常见面试点:hash碰撞

什么是hash碰撞?就是产生的hash值重复了,既然是产生1-16,那么重复的概率还是很高的,没有重复,就按照上面所写的,我新建一个节点就好了,但是重复了呢?就是else的内容了,在此之前我们先明确思路,再去看代码就简单的很了

1,要追加的地方,本身还没有链表,要添加的是第一个

2,我要追加的可能不是链表,可能是红黑树,那我直接转变成树的节点就好

3,后面有链表,那就需要我不停的去遍历,然后找到合适的位置,但是因为是新添加节点,我们还要考虑链表长度达到了8,就要转变成红黑树。

else {
    Node<K,V> e; K k;
//情况1
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
        e = p;
//情况2  转变为树的节点
    else if (p instanceof TreeNode)
        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//情况3  循环查找链表查找位置
    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;
            }
            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;
        afterNodeAccess(e);
        return oldValue;
    }
}
​
​
​
​
++modCount;
if (++size > threshold)
    resize();
afterNodeInsertion(evict);
return null;

不知道根据上面的注释大家是否有了理解

++modCount;
if (++size > threshold)
    resize();

最后几行,因为我们随时要记录长度,准备扩容,最后几行的目的就是来判断是否要扩容的.。

扩容,resize()也是经常被问到的,我们也去看下,还是先说思路;

1,扩容就是达到了指定长度后,每次扩容2倍,16会变成32,但是如果容器本身超过了最大限制,就没法扩容了

2,扩容后会对现有数据重新排序,为什么呢?和生活一样,我们住的空间大了,那我们的行李用品也要搬一些到新空间,别显得那么拥挤,但又不是全部搬走,会选一部分搬,至于怎么选呢?就体现出二进制算法的精妙之处了,具体如何实现,往后看

Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//oldcap为0说明是初始化,不要忘记resize的作用是初始化和扩容
if (oldCap > 0) {
   //长度超出了最大值,无法扩容
    if (oldCap >= MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return oldTab;
    }
//扩容后的值不超过最大值,就扩容2倍
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
             oldCap >= DEFAULT_INITIAL_CAPACITY)
        newThr = oldThr << 1; // double threshold
}
//初始化操作
else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;
else {               // zero initial threshold signifies using defaults
//重新计算加载因子等
    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;

刚才也提到扩容后重新排序的,感兴趣的自己去研究一下,下面是代码

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;
            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) {
                        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;
                }
                if (hiTail != null) {
                    hiTail.next = null;
                    newTab[j + oldCap] = hiHead;
                }
            }
        }
    }
}
return newTab;

但是要提醒的是,刚才说到得巧妙之处是

if ((e.hash & oldCap) == 0

此处的&运算就是精密之处,通过二进制运算来比较现有的与原来的不同,会产生0和1,0就留下,1就移走,移走后的位置下标是 原先位置+扩容长度

才能有限,源码就带大家看到这,那我们进入最后一个环节,问几个问题,看看能否回答?

1,hashmap的底层原理,数据结构是什么?

底层是哈希表(数组加链表),1.8之后引入了红黑树

2,能说一下hashmap的put过程吗?

1>传入key-value,并根据key求出哈希值,用于计算下标
2>查看是否冲突,不冲突就装入容器中,
3>冲突就追加到链表中,并且要查看是否达到链表阈值,达到要转换成红黑树
4>查看节点是否重复,重复就覆盖
5>查看容器是否要扩容
3,hashmap是如何获得下标的?

(n-1) & hash. 原理是高16位不变,与低16位做与或运算
4,能说下扩容吗?扩容后部分数组位置肯定要变化,变成什么了呢?

1>判断当前容量或扩容后的容量是否超出最大值,超出则无法扩容,否则扩容会增加2倍
2>重新遍历数组,然后将部分移动到新位置
(注意:resize还有初始化操作,如果记录的table常量是空就初始化)

位置就是 原有位置+扩容数量
5,hashmap中的链表太长,查找时间复杂度可能会达到0(n),如何解决?

引入红黑树就是为了解决这个问题

大家如果觉得有帮助,请关注公众号: 徒步归行 ,我们会奉献更多的技术文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值