HashMap底层原理

为什么要学HashMap底层原理?

因为最近面试被问到

1.HashMap底层是怎么实现的?

2.数组的初始化长度是多少?

3.为什么数组的每次扩容长度是2的整数倍?

4.HashMap的什么时候扩容?

5.HashMap的加载因子是多少?

6.装载因子为什么是0.75?

等一系列问题,当时被问的哑口无言,面试老师说,这些都不知道,你还说你精通java。

之前自己学习觉得,会用就行了,为什么要了解他这些东西。emmemem...

好了,废话不多说,来说下这个底层原理吧,先上一段简单的代码,真的很简单。。。。

public static void main(String[] args) {
    HashMap<Integer ,String> map =new HashMap<Integer ,String>();
    map.put(1,"张三");
    map.put(2,"李四");
    map.put(4,"王二");
    map.put(1,"非月");
    System.out.println(map);
  }

 输出:{1=非月, 2=李四, 4=王二}

特此说明这里是讲解的jdk1.8,与jdk1.7有所不同。如果要了解jdk1.7,请看别的。

介绍之前,先来看看HashMap这个类,这个类实现了AbstractMap,Cloneable,Map接口,属于双列集合。这里实际上AbstractMap里面也实现了Map接口,但为什么又再去实现Map,这个咱也不知道大神为啥这么写,这个不是我们研究的,暂且先不提。

来看看这个类里面,定义了很多变量

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //这个“<<”代表左移4位,相当于2的4次方,值是16,这个表示数组的初始长度。

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;//当数组的格式达到八个,并且节点的个数达到64个时,就会树化

了解了这些,我们就来根据上面的代码。

第一句创建了一个HashMap,底层什么也没干,初始化了一下

loadFactor这个变量,源码如下:

接着,调用了put方法,里面调用了putVal方法,在这里我们看到,这里将传进来的k值,进行hash,得到一个hash值,然后将这个hash值,key,value传入了putVal

 接下来看看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;

        if ((tab = table) == null || (n = tab.length) == 0)
//判断tab是否为空,put第一个值得时候,这个tab肯定为空,所以第一次进入这里,调用resize()方法,此处实际上只是创建了一个Node数组
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
//如果不为空,那就用下标(n - 1) & hash进行计算,这里第一次n=16,得到一个下标为i的值,看tab这个位置的值为空,就创建一个新的Node节点,这个Node是HashMap里面的一个内部类,这里创建是一个Node对象,将这个node对象直接放在数组中,从这里我们其实也看出来来了,jdk8的数组是一个Node类型的数组
            tab[i] = newNode(hash, key, value, null);
        else {
//不是第一次put,(n - 1) & hash位置也有值
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
//判断将要put的值hash值是否与已经存在的元素的hash相同,并且k也相同,就会到①的位置,直接将要插入的值替换老的值,这也可以验证我们上面的代码,put了 map.put(1,"张三");和 map.put(1,"非月");但最终输出k为1的只有非月
                e = p;
            else if (p instanceof TreeNode)
//判断下一个节点是否是树节点,如果是树节点,就转换成数节点put进入
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
//否则直接插入链表,怎么插入呢,是原有元素的next值指向新的元素,所以是尾插法
                        p.next = newNode(hash, key, value, null);
//判断是否需要树化,个数是否大于等于8-1
                        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;
                }
            }
            //① 进行替换,k是不变,v替换了
            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;
    }

通过上面的源码我们可以总结出:

1.调用put方法后,HashMap会将传入的k值进行hash,这样得到一个值,这个值就是他所在的Node[]中的位置。

2.判断Node[]这个数组是否为空,如果为空则调用resize()方法,具体在这里干了啥。

3.用下标i=(n-1)&hash判断这个位置是否有元素:

          如果没有,直接创建一个Node对象,里面包含hash,key,value,next等,将这个新元素put到数组i位置。

            如果有,在进行新元素和已有的元素的hsah值进行比较。

                        如果将要插入的元素和已经存在的元素的k的hash值相等,调用equals方法比较这两个k是否也相同,则将要插入的元素的v值替换原来的v值。

                        如果不相等,就判断是否是树类型的节点,如果是,则创建一个树节点,将树节点插入

                        如果不是树,则将这个新节点插入链表的尾部,然后判断是否需要树化。

下面来看下resize()

 final Node<K,V>[] resize() {
//第一次进来,table应该等于null
        Node<K,V>[] oldTab = table;
//第一次进来的话,oldTab等于0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
//初始化树化阈值
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
//判断数组的长度是否大于最大容量,就去int型的最大值,第一次肯定小于最大容量,
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
//oldCap << 1 左移一位,相当于乘以2,这就保证了newThr 这个值永远是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
//设置数组的初始长度为16
            newCap = DEFAULT_INITIAL_CAPACITY;
//设置扩容阈值16*0.75=12
            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;
        @SuppressWarnings({"rawtypes","unchecked"})
//初始化创建了一个Node数组,长度为16,看最后,将数组直接返回出去了
        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;
         //判断当前索引j的位置是否存在元素e
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
//判断e元素后面是否还有元素,其实就是判断是否树化
                    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;
    }

这里总结就是

1.如果是第一次进来,直接创建数组大小为16的Node数组

2.如果当前Node的长度乘以2小于最大长度,并且当前数组的长度大于16,就重新给新的数组长度扩大原来的两倍

3.将原数组的数据复制到新数组,所有元素需要重新计算位置

下面来看看树化的代码

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //当数组的长度64时,进行扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            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);
        }
  }

//树化的真正操作
final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                //将root节点置为黑色(根据红黑树的定义)
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    //判断插入节点在红黑树的哪边
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        //小于root节点,放在左边
                        if ((ph = p.hash) > h)
                            dir = -1;
                        //大于root节点,放在右边
                        else if (ph < h)
                            dir = 1;
                        //等于root节点,经过下面的方法尽心过多次判断,确认是否等于
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        //根据dir判断放在左边还是右边
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                           //放在左边(与root相等,也放在左边)
                            if (dir <= 0)
                                xp.left = x;
                           //放在右边
                            else
                                xp.right = x;
                            //进行平衡操作(下面过程省略) 
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            //将隐藏的双向链表调整头结点
            moveRootToFront(tab, root);
        }


通过上面的源码介绍,我们来回答这几个问题:

1.HashMap底层是怎么实现的?

在jdk8中,底层时封装了一个Node数组+链表+红黑树

2.数组的初始化长度是多少?

Node数组的长度时初始化是16

3.为什么数组的每次扩容长度是2的整数倍?

第一:数组扩容主要是与hash有关,如果是单数,key 值hash后会更容易出现位置冲突,但是2的倍数就不容易出现,减少位置冲突。

第二:在resize方法中,代码:

 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }

注定了只能是2的整数倍

4.HashMap的什么时候扩容?

当数组的长度大于 初始值16*加载因子0.75=12的时候就开始扩容

5.HashMap的加载因子是多少?

加载因子是0.75

6.装载因子为什么是0.75?

当加载因子是1时,空间得到很好的应用,但是数据多了容易产生碰撞,而且链表会很大,耽误查询

如果加载因子时0.5,那么减少了对空间的利用,但是查询速度块

所以0.75是经验值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值