jdk8 HashMap1.8源码解析

 

hashmap的数据结构为数组、链表+红黑树(在链表节点数量超过8时链表会变成红黑树),如下图

jdk8 HashMap1.8源码解析_第1张图片

那每个格子里面到底是存储的什么东西呢?hashmap肯定是存储的key/value结构的数据

里面存储了key、value,既然是链表有节点,那是不是就应该有节点指示下一个节点的指示?还有当前的位置?不然当取得时候如何存取?

总共4个属性:hash,key,value,next,hash表示当前格子的位置,next表示指向下一个节点,是一个单向链表。那么在java中以面向对象的概念来说,哪些比较适合呢?entry、node

看看hashmap的源码,node为内部类:

jdk8 HashMap1.8源码解析_第2张图片

这个就是每个格子,也就是node

那既然有数组,数组是怎么表示呢?正常的推理就是node数组 Node[],就是下图,在hashmap默认初始化的时候,默认的长度是16,长度是2^n。必须是2^n,后面会讲

jdk8 HashMap1.8源码解析_第3张图片

那当存储的时候,怎么存储?可能有人说使用new Random().nextInt(0,15)

当使用Random这种方式的时候,第一次存储存储在1的位置上,第二次如果还是1呢?那第三次如果还有可能是1呢?就会出现以下的情况,全部放在1的下标桶里面。一个桶出现很多很多的节点。如下图

jdk8 HashMap1.8源码解析_第4张图片

如果按照上面的设计,那是不是其他的全部都是空着的,节点分布也不均匀,jdk设计作者肯定不会这样来设计。

那就要另寻他法,用其他的算法来设计===》hash算法

通过hash算法来确定一个整形数,put的时候,会询问数组,如果当前桶是空着的,直接放入桶里面,如果发生hash碰撞,则放入链表节点,这样还是不能避免不重复,但能分布得更均匀。

当一个桶的链表节点太长的时候,会将深度改为红黑树,红黑树也算一种二叉树。

注意在jdk7及以下是没有红黑树的,在jdk8版本jdk的设计作者才加入了这个算法。看下图

jdk8 HashMap1.8源码解析_第5张图片

红黑树的要求,以及满足红黑色的条件:

  1. 每个节点要么是红色,要么是黑色。
  2. 根节点必须是黑色
  3. 红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色)。
  4. 对于每个节点,从该点至null(树尾端)的任何路径,都含有相同个数的黑色节点。

 

什么时候转换成红黑树?

当超过8个节点的时候,会生成红黑树的树状结构

jdk8 HashMap1.8源码解析_第6张图片

当节点数量6及以下的时候,会还原成链表

jdk8 HashMap1.8源码解析_第7张图片

put()源码解析

Map hashMap = new HashMap();
		hashMap.put("ypp", 666);
 /**
     * Constructs an empty HashMap with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

当new hashmap的时候,并没有初始化,只是初始化了一个负载因子,这是为了扩容而准备的

进入hashmap的put方法源码:

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node[] tab; Node p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        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;
    }

进入putVal()方法,首先判断table是否为空等,当put的时候,当前数组其实是一个空的。这个table就是Node[],在文章最前面有提到,那么会进入到n = (tab = resize()).length;这一行代码。resize()就是去初始化数组去了,接着看源码

final Node[] resize() {
        Node[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            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;
        @SuppressWarnings({"rawtypes","unchecked"})
            Node[] newTab = (Node[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node 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)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node loHead = null, loTail = null;
                        Node hiHead = null, hiTail = null;
                        Node 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;
    }

会进入到以下两行代码,以下两行代码,不用说,都能看懂,初始化数组的长度16和扩容初始化12,其他代码先不看

newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);

jdk8 HashMap1.8源码解析_第8张图片

这里使用的位运算,1向左位移4==》10000  转换成10进制就是16,然后16*负载因子0.75=12.

在我们平时写代码的时候基本都是十进制,但计算机十进制最后还是会转换为二进制,所以它直接使用了二进制提高效率,虽然一个地方不起眼,但是作为一个框架或者一门语言,地方多了,效率可想而知是可以提高不少的。但是在开发中建议不要用二进制去增加开发的复杂度,毕竟开发用的地方并不多!

 

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

当初始化完成后回到刚才的put()方法,有一个hash(key)的方法传入putVal()这个方法里面,hash(key)的位置得出一个hash值,这里还是使用的位移+异或运算,喜欢看源码的同学会发现很多的框架等都会使用位移以提高效率。

 

回到putVal()方法,比较重要的源码点

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

得出来的hash这里用到了与运算,为什么还要-1?并且不是使用的hash%16的这种方式

n-1=15  n就是数组的长度  15的二进制为01111   

之前计算的hash值右移16位不管怎么样最多都是 1010110101110110111011101011011这样的格式,和01111进行与运算 

1010110101110110111011101011011   

                                                  01111

不管如何换成十进制最小0最大还是15,那为什么它要使用与运算而不是hash%16取模呢?一个字:快,两个字:效率

if ((p = tab[i = (n - 1) & hash]) == null)     

这里判断就是这个桶下标是否为空,如果为空,当前node直接放进去:tab[i]=newNode(hash, key, value, null); null是节点指向下一个节点。这里为空是因为当前就这一个节点,并没有其他节点,当后面如果有其他节点进来,会改变它的值指向下一个节点

如果不为空,进入以下else代码:

        else {
            Node e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        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;
代码1         if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
代码2       if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

代码1当hash值一样的时候,key相同。代码2直接替换并返回

代码3
              else if (p instanceof TreeNode)
                e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);

代码3判断当前这个节点下面是否为红黑树,进行红黑树相应处理,下面截图可以看到父节点、左节点、右节点以及红色节点等

jdk8 HashMap1.8源码解析_第9张图片

代码4

                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        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;
                }

代码4就是链表的情况,循环当前下标所有节点,判断哪个节点的next为空,就把自己放入到那个节点后面去

                  if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);

如果节点数量超过了8,就把当前链表转换为红黑树,并跳出循环

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);

jdk8 HashMap1.8源码解析_第10张图片

 

扩容

肯定会存在着不够用的情况,那如何扩容?

resize()

jdk8 HashMap1.8源码解析_第11张图片

putVal()

jdk8 HashMap1.8源码解析_第12张图片

在初始化的时候,hashmap内部记录了当前扩容的相关参数

在put添加数据的时候,putVal()方法结尾判断了当前数组实际使用大小,当初始化的hashmap长度16,使用超过了12会再次调用resize()方法进行扩容扩容是以2倍扩容,来保证长度必须是2^n。16变为32 threshold变为24.当put的时候还是上面的流程。

所以resize()方法有两个主要功能,初始化和扩容

扩容之后会重新计算,分布节点

 

以上只是hashmap其中的一部分,主要的,写了半个多小时。时间有限,已经凌晨1点半,就不写了。如果帮助到你,点个赞

 

最后附上阿里相关面试题:

数据结构

  1. HashMap的原理,内部数据结构?
    • 底层使用哈希表(数组 + 链表),当链表过长会将链表转成 红黑树以实现 O(logn) 时间复杂度内查找
  2. 讲一下 HashMap 中 put 方法过程?
    1. 对 Key 求 Hash 值,然后再计算 下标。
    2. 如果没有碰撞,直接放入桶中,
    3. 如果碰撞了,以链表的方式链接到后面,
    4. 如果链表长度超过阀值(TREEIFY_THRESHOLD == 8),就把链表转成红黑树。
    5. 如果节点已经存在就替换旧值
    6. 如果桶满了(容量 * 加载因子),就需要 resize。
  3. HashMap 中 hash 函数怎么是是实现的? 还有哪些 hash 的实现方式?
    1. 高 16bit 不变,低 16bit 和高 16bit 做了一个异或
    2. (n - 1) & hash --> 得到下标
  4. HashMap 怎样解决冲突,讲一下扩容过程,假如一个值在原数组中,现在移动了新数组,位置肯定改变了,那是什么定位到在这个值新数组中的位置,
    • 将新节点加到链表后,
    • 容量扩充为原来的两倍,然后对每个节点重新计算哈希值。
    • 这个值只可能在两个地方,一个是原下标的位置,另一种是在下标为 <原下标+原容量> 的位置。
  5. 抛开 HashMap,hash 冲突有那些解决办法?
    • 开放定址,链地址法
  6. 针对 HashMap 中某个 Entry 链太长,查找的时间复杂度可能达到 O(n),怎么优化?
    • 将链表转为红黑树, JDK1.8 已经实现了。
  7. 数组和 ArrayList 的区别;
    1. 数组可以包含基本类型和对象类型,ArrayList 只能包含对象类型
    2. 数组大小固定,ArrayList 大小可以动态变化
    3. ArrayList 提供了更多的特性(addAllremoveAll)。
  8. Arraylist 如何实现排序
    • Collections.sort(List list);
    • sort(List list, Comparator c);
  9. HashMap ,HashTable 区别
  10. HashMap、ConcurrentHashMap 区别。
    • ConcurrentHashMap 两个 hash 过程,第一次找到所在的桶,并将桶锁定,第二次执行写操作。
  11. ConcurrentHashMap原理,jdk1.8 后有哪些改变(引入CAS等)
  12. TreeMap 和 TreeSet 区别和实现原理
    • TreeSet 底层是 TreeMapTreeMap 是基于红黑树来实现的。
  13. 红黑树的特点及相比平衡二叉树的优点(先介绍各自特点)?
    • 红黑树
      1. 每个节点要么是红色,要么是黑色。
      2. 根节点永远是黑色的。
      3. 所有的叶节点都是空节点(即 null),并且是黑色的。
      4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)
      5. 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。
    • 平衡二叉树
      1. 任何节点的两个儿子子树的高度最大差别为一
    • 红黑树并不追求“完全平衡”——它只要求部分地达到平衡要求,降低了对旋转的要求,从而提高了性能。
  14. B+树的了解
    • 多分支结构有效降低了树的高度
    • B 树的各种操作能使 B 树保持较低的高度,从而达到有效避免磁盘过于频繁的查找存取操作,从而有效提高查找效率
  15. Trie-Tree 原理及其应用;
    • 字典树
    • 特点
      1. 根节点不包含字符,除根节点外的每一个子节点都包含一个字符。
      2. 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。
      3. 每个节点的所有子节点包含的字符互不相同。
    • 核心思想是空间换时间
    • 应用
      1. 字符串检索
      2. 词频统计

 

你可能感兴趣的:(java)