zoukankan      html  css  js  c++  java
  • HashMap源码解析(只为吊打面试官)

    现在的面试当中,大家经常会被问到关于HashMap的问题,而且这个集合在开发中也经常会使用到,也相当的重要。

    写这篇文章的期望:一文搞定HashMap的所有相关知识和面试问题,从此不再惧怕HashMap,和面试官大战三百回合。

    本文是基于jdk1.8来分析的,篇幅较长。会循序渐进的往文章中添加或修改内容,有错误之处请指出,一起成长。

    这篇文章,希望可以解决以下问题。

    * 认识HashMap

    * HashMap的底层数据结构及其演进过程

    * 什么是红黑树

    * HashMap如何存储一个元素

    一、认识HashMap

    HashMap最早是在jdk1.2中开始出现的,一直到jdk1.7都没有太大的变化。但是到了jdk1.8突然进行了一个很大的改动。

    其中一个最显著的改动就是:之前jdk1.7的存储结构是数组+链表,到了jdk1.8变成了数组+链表+红黑树。

    另外,HashMap是非线程安全的,也就是说在多个线程同时对HashMap中的某个元素进行增删改操作的时候,是不能保证数据的一致性的。

    深入分析HashMap

    二、HashMap的底层数据结构及其演进过程

      HashMap在jdk1.7时的存储结构图

     从上图我们可以看到,在jdk1.7中,首先是把元素放在一个个数组里面,后来存放的数据元素越来越多,于是就出现了链表,对于数组中的每一个元素,都可以有一条链表来存储元素。这就是有名的“拉链式”存储方法。

    就这样用了几年,后来存储的元素越来越多,链表也越来越长,在查找一个元素时候效率不仅没有提高(链表不适合查找,适合增删),反倒是下降了不少,于是就对这条链表进行了一个改进。如何改进呢?就是把这条链表变成一个适合查找的树形结构,没错就是红黑树。于是HashMap的存储数据结构就变成了下面的这种。

    注意:不是说变成了红黑树效率就一定提高了,只有在链表的长度不小于8,而且数组的长度不小于64的时候才会将链表转化为红黑树。

    那么为什么不一下把整个链表变成红黑树呢?

    这个问题也可以转化为为什么非要等到链表的长度大于等于8的时候,才将链表转化为红黑树?

    这里从两方面来说明:

    1 构造红黑树要比构造链表复杂,在链表的节点不多的时候,从整体的性能看来, 数组+链表+红黑树的结构可能不一定比数组+链表的结构性能高。就好比杀鸡焉用牛刀的意思。

    2 HashMap频繁的扩容,会造成底部红黑树不断的进行拆分和重组,这是非常耗时的。因此,也就是链表长度比较长的时候转变成红黑树才会显著提高效率。

     什么是红黑树

    红黑树是一个自平衡的二叉查找树,也就是说红黑树的查找效率是非常的高,查找效率会从链表的o(n)降低为o(logn)。如果之前没有了解过红黑树的话,也没关系,你就记住红黑树的查找效率很高就OK了。

    HashMap如何存储一个元素

    HashMap存储元素的一种方式,测试代码:

    public class HashMapTest {
        public static void main(String[] args) {
            HashMap<String, Integer> map= new HashMap<>();
            //存储一个元素
            map.put("shenhaha", 18);
        }
    }
    

    在这里HashMap<String, Integer>,第一个参数是键,第二个参数是值,合起来叫做键值对。存储的时候只需要调用put方法即可。那底层的实现原理是怎么样的呢?这里还是先给出一个流程图

    转换为文字步骤描述:

    (1)第一步:调用put方法传入键值对

    (2)第二步:使用hash算法计算hash值

    (3)第三步:根据hash值确定存放的位置,判断是否和其他键值对位置发生了冲突

    (4)第四步:若没有发生冲突,直接存放在数组中即可

    (5)第五步:若发生了冲突,还要判断此时的数据结构是什么?

    (6)第六步:若此时的数据结构是红黑树,那就直接插入红黑树中

    (7)第七步:若此时的数据结构是链表,判断插入之后是否大于等于8

    (8)第八步:插入之后不大于8,那么就直接插入到链表尾部即可

    (9)第九步: 插入之后大于8了,就要先调整为红黑树,在插入

    上面就是插入数据的整个流程,光看流程还不行,我们还需要深入到源码中去看看底部是如何按照这个流程写代码的。

    鼠标聚焦在put方法上面,ctrl一下,进入put的源码。来看一下:

        public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }

    可以看到,put方法内部调用的是putVal方法。putVal方法有5个参数:  

    (1)第一个参数hash:调用了hash方法计算hash值

    (2)第二个参数key:就是我们传入的key值,也就是例子中的张三

    (3)第三个参数value:就是我们传入的value值,也就是例子中的20

    (4)第四个参数onlyIfAbsent:也就是当键相同时,不修改已存在的值

    (5)第五个参数evict :如果为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;
            //第一部分
            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<K,V> 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<K,V>)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;
        }
    

      

  • 相关阅读:
    《ASP.NET Core跨平台开发从入门到实战》Web API自定义格式化protobuf
    .NET Core中文分词组件jieba.NET Core
    .NET Core 2.0及.NET Standard 2.0
    Visual Studio 2017 通过SSH 调试Linux 上.NET Core
    Visual Studio 2017 ASP.NET Core开发
    Visual Studio 2017正式版离线安装及介绍
    在.NET Core 上运行的 WordPress
    IT人员如何开好站立会议
    puppeteer(二)操作实例——新Web自动化工具更轻巧更简单
    puppeteer(一)环境搭建——新Web自动化工具(同selenium)
  • 原文地址:https://www.cnblogs.com/shenhaha520/p/12083563.html
Copyright © 2011-2022 走看看