没想到你是这样HashMap!!

没想到你是这样HashMap!!

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

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

3d4698abfd68423649b6ba2f9c24a9f3.jpeg


开始以前请你们思考一下,咱们常见的数据结构有哪些?典型的表明又有哪些呢?算法

01 常见数据机构数组

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

典型的表明:app

数组:相似以下,典型表明是Arraylist和Vectorless

aa269a4e84170fc3d97e9b4c3f69bcfb.jpeg


链表:典型表明是LinkedListide

双向:this


2d3ed9755eda41381ed5886702945529.jpeg

单向3d

a47265b96b5f753326f5cbca89b98882.jpeg

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


07bdddf80e4368aeb37b54a7e1332d27.jpeg


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,的时候,就开始扩容。

d104da66ddf309fd68889dd812587487.jpeg


  • 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的过程




a0f59ce4731c97743d3fb85b46101200.jpeg


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

5f5716f70f390fdc50baffebc9c3afff.jpeg

咱们看到 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的内容了,在此以前咱们先明确思路,再去看代码就简单的很了

e6bde6faae20fbd6c2be439363eb133f.jpeg

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();

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

641a4831e86b632cf8d27adce7255bed.jpeg

扩容,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就移走,移走后的位置下标是  原先位置+扩容长度


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

c22d38155136f6bd669cd88861c119c1.jpeg

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),如何解决?

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


原做者:归行-泰然
原文连接: 没想到你是这样HashMap!!
原出处:公众号
侵删

b30a8a37452e6e5b316070f3ef596f4e.jpeg

相关文章
相关标签/搜索