hashmap存多少条数据_干货 | 面试官想问的HashMap,都在这一篇里面了!

9c3265bc46d6a1564476de420f5e8cf5.gif

来源公众号:非科班的科班

本文思维导图

68ce265ffb20f579ee0444a6d06f20c1.png

HashMap简介

HashMap 是很常用的一种集合框架,其底层实现方式在 JDK 1.7和 JDK 1.8中却有很大区别。HashMap 是用来存储数据的,它底层在JDK 1.7是数组+链表实现的,而JDK 1.8是使用数组+链表+红黑树实现,通过对 key 进行哈希计算等操作后得到数组下标,把 value 等信息存放在链表或红黑树存在此位置。如果两个不同的 key 运算后获取的数组下标一致,就出现了哈希冲突。数组默认长度是16,如果实际数组长度超过一定的值,就会进行扩容。HashMap的面试不管小厂还是大厂都是高频问点,特别是大厂一定会深究底层,采用持续的追问,知道你怀疑人生,在Java7和Java8中对HashMap的数据结构进行了很大的优化。今天这篇文章就以HashMap的高频问点为主,层层的剖析HasMap的底层实现,话不多说,直接进入正题。问点一:你了解HashMap的底层数据结构吗?对于HashMap的底层数据结构在Java7和Java8中的实现是不同的,在Java7中是采用数组+链表的数据结构进行实现,而在Java8中是采用数组+链表+红黑树的数据结构实现的。说时迟那时快,刚话说完,从兜里拿出笔和纸,啪地一声放在桌子上画了起来,许久之后,出现了两幅jdk7和jdk8的HashMap的内部结构图:
9486ed4494d97ceeb610d7953cb2d22e.png
上图是jdk7内部结构图,以Entry[]数组作为哈希桶,每个哈希桶的后面又可以连着一条单向链表,在链表中以k,v的形式存储数据,并且每一个节点有指向下一节点的指针。51eebaec066f501608681df502da446c.png上图是jdk8的HashMap的内部结构图,此时在源码源码中就不再使用Entry[]作为数组,而是使用Node[]数组作为哈希桶,每个哈希桶的后面也可能连着一条单向链表或者红黑树。当单向链表的值>8的时候,链表就会转换为红黑树进行存储数据,在面试大厂的时候,其实答到这里,还是不完整的,为什么呢?因为你想你说的上面的实际由jdk7和jdk8转变的一个结果,但是重要的为什么要这样做?你还没有回答。如果你聪明点的话,就不会等着面试官抛出接下来的问题?而是自己去回答这个为什么?不是等着面试官继续抛出这个为什么?一个会聊天的人他会去猜测对方想知道什么?问点二:哈希冲突是怎么回事?HashMap又是怎么解决的?哈希冲突是怎么回事呢?当的数据将要存进HashMap中的时候,会先,把k值经过hash函数进行计算得到hash值,再通过hash值进行计算得到数据在数组的下标,在jdk7中的源码如下:
//key 进行哈希计算
int hash = hash(key);
//获取数组下标
int i = indexFor(hash, table.length);
通过计算后的下标,从而得到数组的对应下标的位置,最后把k,v值存进去,同样的当再次第二次存值的时候,同样把k,v传进来,当k再次进行计算出数组下标index,有可能和第一次计算的index的值相同。为什么有可能相同呢?这个是hash函数的原因,看完上面推荐的那篇hash函数详细介绍你就懂了。当两次的计算index相同,这就是hash冲突。但是,两次的需要存进去的value值是不同的,这就出现了同一个数组后面有一条链表,会比较链表上的每一个value值与当前的value是否相同,若是不相同,通过头插法,将数值插入链表中。如下图所示:
ee4d2e6a37ca357cea8508f7130a6b4a.png
接下来通通过源码进行分析,在jdk 7插入的put 方法源码如下:
public V put(K key, V value) {
     //数组为空就进行初始化
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
     //key 进行哈希计算
        int hash = hash(key);
     //获取数组下标
        int i = indexFor(hash, table.length);
     //如果此下标有值,遍历链表上的元素,key 一致的话就替换 value 的值
        for (Entry e = table[i]; e != null; e = e.next) {
            Object k;if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);return oldValue;
            }
        }
        modCount++;//新增一个key
        addEntry(hash, key, value, i);return null;
    }
put方法中主要做了以下几件事:
  1. 判断table数组是否为空,若为空进行初始化table数组。
  2. 判断key值是否为null,将null是作为key存进去。
  3. 若key不为空,通过key计算出数组下标,判断table[i]是否为空。
  4. 若是不为空通过链表循环,判断在链表中是否存在与该key相等,若是存在,直接将value替换成新的value。若是table[i]为空或者链表中不存在与之相同的key,就addEntry(hash, key, value, i)新增一个节点。
接下来看看addEntry(hash, key, value, i)新增节点的源码如下:
void addEntry(int hash, K key, V value, int bucketIndex) {
//数组长度大于阈值且存在哈希冲突(即当前数组下标有元素),就将数组扩容至2倍
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(hash, key, value, bucketIndex);
    }
这个方法很简单,直接就是判断当前数组的大小是否>=threshold并且table[bucketIndex]是否为null。若成立扩容,然后rehash,重新得到新数组的下标值,最后 createEntry(hash, key, value, bucketIndex)创建新节点。最后来看一下createEntry(hash, key, value, bucketIndex)创建新节点的源码如下:
void createEntry(int hash, K key, V value, int bucketIndex) {
  //此位置有元素,就在链表头部插入新元素(头插法)
        Entry e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }
该方法就是通过头插法加入新节点,方法非常简单,相信都能看懂。经过上面对put方法的源码分析,在jdk 7 中put操作的原理图如下所示:
a24ceb21e171f6f7f2de21c88626d6ca.png
在JDK 7中,链表存储有一个缺点,就是当数据很多的时候,链表就会很长,每次查询都会遍历很长的链表。因此在JDK 8中为了优化HashMap的查询效率,将内部的结构改为数组+链表+和红黑树,当一个哈希桶后面的链表长度>8的时候,就会将链表转化为红黑树,红黑树是二分查找,提高了查询的效率。接下来通过JDK 8的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;//key 相同就覆盖原来的值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);//链表长度超过8,就把链表转为红黑树if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);break;
                    }//key相同就覆盖原来的值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. 判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)。
  2. 根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。
  3. 如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key是否相等,相等就赋值给 e。
  4. 如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
  5. 如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。
  6. 接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
  7. 如果在遍历过程中找到 key 相同时直接退出遍历。
  8. 如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。
  9. 最后判断是否需要进行扩容。
继续看下 treeifyBin 的源码:
final void treeifyBin(Node[] tab, int hash) {
        int n, index; Node e;//链表转为红黑树时,若此时数组长度小于64,扩容数组if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode hd = null, tl = null;//链表转为树结构do {
                TreeNode 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);
        }
    }
由此可以看到1.8中,数组有两种情况会发生扩容:
  1. 一是超过阈值
  2. 二是链表转为红黑树且数组元素小于64时
由此在jdk1.8中,默认长度为16情况下,要么元素一直放在同一下标,链表转为红黑树且数组元素小于64时就会扩容,要么超过阈值12时才会扩容。依据上面的源码分析,在JDK 1.8中put方法的执行的原理图如下:
c8a8a0dd98cc7b18c096a506f7a16b9b.png
通过上面的分析,我们可以看到jdk1.7和1.8情况下 hashmap实现方式区别是非常大的。在源码的分析中,也可以找到下面问题的答案。问点三:HashMap的扩容机制是怎么样的?JDK7与JDK8有什么不同吗?JDK 1.7的扩容条件是数组长度大于阈值且存在哈希冲突,在JDK 7中的扩容的源码如下:
void addEntry(int hash, K key, V value, int bucketIndex) {
     //数组长度大于阈值且存在哈希冲突(即当前数组下标有元素),就将数组扩容至2倍
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(hash, key, value, bucketIndex);
    }
而JDK 1.8扩容条件是数组长度大于阈值或链表转为红黑树且数组元素小于64时,源码中的体现如下所示:
//数组长度大于阈值,就扩容
if (++size > threshold)
      resize();

//链表转为红黑树时,若此时数组长度小于64,扩容数组
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
      resize();
问点四:HashMap中的键值可以为Null吗?能简单说一下原理吗?在JDK7中是允许null存进去的,通过 putForNullKey(value)方法来存储key为null值,具体的实现的源代码如下:
if (key == null)
    return putForNullKey(value);
而在JDK 8中当传进key为null值的时候,就直接将hash值取0,进行计算存入值的位置。
public V put(K key, V value) {
  return putVal(hash(key), key, value, false, true);
}

static final int hash(Object key) {
  int h;
  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
问点五:HashMap中能put两个相同的Key吗?为什么能或为什么不能?这个问题比较简单,在JDK7和JDK8中的做法是一样的,若是存入的key值一样,就会将原来的key所对应的value值直接替换掉,可以从源码中看出:
// JDK1.7
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
       V oldValue = e.value;
       // 直接替换原来的value值
       e.value = value;
       e.recordAccess(this);
       return oldValue;
 }

// JDK 1.8
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;
        }
        // 存在key值相同
        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)
        // 替换掉原来value值
        e.value = value;
    afterNodeAccess(e);
    return oldValue;
}
问点六:聊一聊JDK 7的HashMap中的“死锁”是怎么回事?HashMap是线程不安全的,在HashMap的源码中并未对其操作进行同步执行,所以在并发访问的时候就会出现线程安全的问题。由于上一篇的ConcurrentHashMap篇中讲到了死锁,也画了图,但是很多读者说看不懂,这里我的锅,在这里详细的进行图解。假设:有线程A和线程B,并发访问HashMap中的数据。假设HashMap的长度为2(这里只是为了讲解方便假设长度为2),链表的结构图如下所示:
cca17a9c43ad1ecaead8e29aa08d8cad.png
4和8都位于同一条链表上,其中的threshold为1,现在线程A和线程B都要进行put操作,首先线程A进行插入值。此时,线程A执行到transfer函数中(transfer函数是resize扩容方法中调用的另一个方法),当执行(1)位置的时候,如下所示:
/**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry e : table) {while(null != e) {
        Entry next = e.next; ---------------------(1)if (rehash) {
            e.hash = null == e.key ? 0 : hash(e.key);
        }int i = indexFor(e.hash, newCapacity);
        e.next = newTable[i];
        newTable[i] = e;
        e = next;
    } // while
    }
}
此时线程A挂起,在此时在线程A的栈中就会存在如下值:
e = 4
next = 8
此时线程B执行put的操作,并发现在进行put操作的时候需要扩容,当线程B执行 transfer函数中的while循环,即会把原来的table变成新一table(线程B自己的栈中),再写入到内存中。执行的过程如下图所示(假设两个元素在新的hash函数下也会映射到同一个位置):
1d9e4bd58218276fe11a9c02f1780040.png
此时线程A有获取到cpu的执行时间,接着执行(但是纤层A中的数据仍是旧表数据),即从transfer代码(1)处接着执行,当前的 e = 4, next = 8, 上面已经描述,执行的的过程若下图所示:
3d9ccc3c14c05d49d5059fbb1eb76812.png
当操作完成,执行查找时,会陷入死循环!问点七:HashMap是线程安全的吗?为什么安全或者不安全?从上图JDK8的put操作原理图中可以看出为HashMap的put方法的详细过程,其中造成线程不安全的方法主要是resize(扩容)方法。假设,现在有线程A 和线程B 共同对同一个HashMap进行put操作,假设A和B插入的Key-Value中key的hashcode是相同的,这说明该键值对将会插入到Table的同一个下标的,也就是会发生哈希碰撞。此时HashMap按照平时的做法是形成一个链表(若超过八个节点则是红黑树),现在我们插入的下标为null(Table[i]==null)则进行正常的插入。此时线程A进行到了这一步正准备插入,这时候线程A堵塞,线程B获得运行时间,进行同样操作,也是Table[i]==null 。此时它直接运行完整个put方法,成功将元素插入。随后,线程A获得运行时间接上上面的判断继续运行,进行了Table[i]==null的插入(此时其实应该是Table[i]!=null的操作,因为前面线程B已经插入了一个元素了),这样就会直接把原来线程B插入的数据直接覆盖了,如此一来就造成了线程不安全问题。-End-49519f8c28be4a39c486deed4d6be82b.gif

90后中年人の爽点大赏

89c8c7198ddf5059a559a7f4cfa718ae.png

拼多多的厕所上了热搜,996的大厂员工没有如厕自由

076df61cd3dad3797780f30b6c2e8823.png

为什么鬼不会攻击被子里的人?

93c1d6bf988566ca03eb12fda83ebb19.png 可乐记得加冰,爱我就要置顶 93c1d6bf988566ca03eb12fda83ebb19.png1292a642fe6161f72765257e6f726c73.gif627458b4cb82244294a0eda52920a850.png

1514ce2353910c6a84788e71f2e59f10.png素质三连biubiubiu~e4219854ccf40119f5cbc235363be037.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/328598.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

StackExchange.Redis客户端读写主从配置,以及哨兵配置

今天简单分享一下StackExchange.Redis客户端中配置主从分离以及哨兵的配置。 关于哨兵如果有不了解的朋友&#xff0c;可以看我之前的一篇分享&#xff0c;当然主从复制文章也可以找到。http://www.cnblogs.com/tdws/tag/NoSql/ 为什么要有这篇分享呢&#xff0c;是因为我之前也…

Windows7下如何设置MyEclipse2014字体大小

转载自 Windows7下如何设置MyEclipse2014字体大小Java开发工具MyEclipse2014&#xff0c;安装完毕后发现字体过小&#xff0c;并且对眼睛不好&#xff0c;该如何设置这个开发工具里整体字体大小。设置MyEclipse字体大小&#xff0c;具体实现看下面详细描述双击“MyEclipse Pr…

项目的包结构 mybatis三剑客

mybatis三剑客 1.mybatis generator 数据库自动生成pojo对象类 xml 是dao接口的实现 service调用dao mybatis gentenrator generatorConfig.xml 直接复制 讲解一下 datasource.properties 直接复制 讲解一下 修改一下dao的实现mapper createTime 和 updateT…

css img 适配尺寸_CSS容易使人蒙圈的几个经典问题

本文摘自这一年来自己在工作中经历的几个比较好的CSS问题(不一定复杂&#xff0c;但个人觉得都挺值得一说)&#xff0c;同时也准备了2个JS问题一、CSS篇1.1 元素默认蓝色边框input标签元素(如button、text 、areatext)的一些事件(如click、focus等)&#xff0c;在很多浏览器下默…

怎样增强MyEclipse的代码自动提示功能

转载自 怎样增强MyEclipse的代码自动提示功能 MyElipse的默认代码提示功能隐藏了许多细节&#xff0c;需要开发者手动设置&#xff0c;一起来设置吧&#xff0c;让你的myeclpse更强大 一般在Eclipse &#xff0c;MyEclipse代码里面&#xff0c;打个foreach&#xff0c;switch等…

第一篇:Dapper快速学习

我们都知道ORM全称叫做Object Relationship Mapper&#xff0c;也就是可以用object来map我们的db&#xff0c;而且市面上的orm框架有很多&#xff0c;其中有一个框架叫做dapper&#xff0c;而且被称为the king of ORM。 一&#xff1a;为什么选择Dapper 1. 性能优越&#xff1a…

配置spring、SpringMVC,mybatis进行整合

springframwork的官网 spring-projects/spring-mvc-showcase https://github.com/spring-projects/spring-mvc-showcase spring-projects/greenhouse https://github.com/spring-projects/greenhouse spring-projects/spring-boot spring-boot https://github.com/sprin…

对应node版本_安装Node.js教程

小编首先下载Node.js,首先打开浏览器&#xff0c;然后输入https://nodejs.org/en/&#xff0c;进入nodejs官网&#xff0c;然后按照下图中红色箭头指示步骤2&#xff0c;这个是nodejs8.12版本&#xff0c;下面Recommended For Most Users,这个就是推荐大家使用的版本&#xff0…

Asp.net Core 使用MyCat分布式数据库,实现读写分离

简介   MyCat2.0版本很快就发布了&#xff0c;关于MyCat的动态和一些问题&#xff0c;大家可以加一下MyCat的官方QQ群&#xff1a;106088787。我们今天主要介绍一下&#xff0c;在我们的Asp.net Core中如何使用Mycat&#xff0c;这源于一个大神&#xff08;Amamiya Yuuko&…

如何在MyEclipse中显示行数

转载自 如何在MyEclipse中显示行数如何在MyEclipse中显示行数呢&#xff1f;myeclipse在默认情况下不显示行数时&#xff0c;用户可以自己设置显示行数&#xff0c;行数可以帮助我们方便的查看代码。首先&#xff0c;打开windows菜单然后选择preference&#xff0c;进入首选项…

logback日志配置

直接复制讲一下好了 比较简单 logback 可以看到执行的sql了 mybatis执行的sql了 <?xml version"1.0" encoding"UTF-8"?> <configuration scan"true" scanPeriod"60 seconds" debug"false"><!-- 打印日…

旅游系统_数字洛江智慧旅游系统助力提升旅游安全水平

长期以来&#xff0c;景区客流监测工作都是个“老大难”问题。为进一步提升旅游安全监管水平&#xff0c;今年洛江区将该项工作列入民办实事项目&#xff0c;投入96万元&#xff0c;大力推进智慧旅游项目建设。日前&#xff0c;洛江区智慧旅游系统正式上线运行&#xff0c;该智…

如何在 ASP.NET MVC 中集成 AngularJS

介绍 当涉及到计算机软件的开发时&#xff0c;我想运用所有的最新技术。例如&#xff0c;前端使用最新的 JavaScript 技术&#xff0c;服务器端使用最新的基于 REST 的 Web API 服务。另外&#xff0c;还有最新的数据库技术、最新的设计模式和技术。 当选择最新的软件技术时&am…

ftp相关配置

ftp # 图片上传的会再讲的 #ftp的相关配置 #ftp.server.ip你的FTP服务器ip地址 ftp.server.ip127.0.0.1 ftp.usermmallftp ftp.passftppassword #ftp.server.http.prefixhttp://img.happymmall.com/.ftp.server.http.prefixftp://mmallftp:ftppassword127.0.0.1:21/img/ #ftp.…

MyEclipse web项目导入Eclipse,详细说明

转载自 MyEclipse web项目导入Eclipse&#xff0c;详细说明最近导入一个MyEclipse的项目&#xff0c;具体是&#xff1a;spring4.1的web项目&#xff0c;同时遇到了一些问题&#xff0c;总结一下。 1、进入项目目录&#xff0c;找到.project文件&#xff0c;打开。 增加一个&…

matlab盒子分形维数_分形:盒子维数

今天主要想说的是&#xff0c;分形中的差分盒子维数的原理&#xff0c;基于分形的基础概念就不在这里说啦.分形维数可以用于定量描述图像表面的空间复杂程度&#xff0c;能够定量的表现图像的纹理特征. 采用不同的维数进行纹理特征描述时&#xff0c;精度有所区别&#xff0c;我…

Eclipse中看不到jsp的页面效果

转载自 Eclipse中看不到jsp的页面效果eclipse打开jsp后&#xff0c;在文件下面部分应该有”design"视图的&#xff0c;你点击一下看看。还是没有的话&#xff0c;在JSP文件上点点右键,"open with"选"webpage editor"&#xff0c;然后下面应该有“des…

唤醒幻数据包禁用会怎么样_如何利用splashtop实现远程开机、远程唤醒电脑

Splashtop商业版和个人版提供了局域网唤醒计算机(WoL)功能&#xff0c;您可以按照下面的指引进行设置。首先&#xff0c;请确保完全满足以下三个条件&#xff0c;否则&#xff0c;远程唤醒无法工作。计算机 BIOS支持WoL并且该选项已启用。Windows或Mac计算机的操作系统中已正确…

第二篇:Dapper中的一些复杂操作和inner join应该注意的坑

上一篇博文中我们快速的介绍了dapper的一些基本CURD操作&#xff0c;也是我们manipulate db不可或缺的最小单元&#xff0c;这一篇我们介绍下相对复杂 一点的操作&#xff0c;源码分析暂时就不在这里介绍了。 一&#xff1a;table sql 为了方便&#xff0c;这里我们生成两个表…