面试必备篇之hashMap篇(吊打面试官)

本文深入探讨HashMap的数据结构和底层原理,对比JDK1.7与JDK1.8的性能,包括HashMap的特点、哈希表存储、扩容机制、以及JDK1.8引入的红黑树,旨在帮助读者理解并优化HashMap的使用。
摘要由CSDN通过智能技术生成

简介以及分析

总所周知,每当我们面试的时候,首先要考我们的主要是有集合属性,在集合属性中我们重点要考的就是map集合,对于Map我们使用的最大的也就是HashMap,提到了HashMap集合的属性,Hashmap在Jdk1.7到Jdk1.8进行升级的时候其底层的数据结构引入了红黑树和扩容的优化等,接下来的文章主要是结合Jdk1.7和Jdk1.8,深入对HashMap的数据结构和底层原理进行相关的功能的探讨。
在这里插入图片描述

HashMap的特点说明
HashMap是根据键的hashCode值存储数据,大多数的时候都是直接根据其的值直接定位到它的值,因此其相关的值是比较快速的,但是其遍历的过程却是没有规律的,HashMap只允许有一条记录的key为null,允许多条记录的value的值为空,HashMap是非线性安全的,即同一时刻是可以多个线程同时写HashMap,可能是导致数据的不一致,如果是有需要可以使用Collections的syschronized方法是HashMap具有线程的安全的能力,或者是使用CurrentHashMap来进行保证。

底层原理

对于我们经常使用的这样的一种数据类型,我们有必要进行相关的功能的研究和判断来彻底来搞懂我们的这个hashMap的底层原理,这个过程能够让我们能够在以后开发的过程中对这种数据结构的使用更加得心应手。

数据结构
对于hashMap而言,Jdk1.7之前的话主要是数组+链表来进行相关的实现的,Jdk1.8的时候开始了以数组+链表+红黑树进行实现的,这个过程中进行相关的功能的研究和实现,下面来让我们进行相关的具体的功能的分析,我们为什么会这样做呢?
在这里插入图片描述
那么我们首先要弄明白的是数据底层我们到底是存储的是什么数据结构,这样的存储有什么优点呢?

  • 从源码上我们可以知道,HashMap有一个重要的字段,就是Node[] table,明显它是一个Node的数组,接下来我们来看下Node[JDK1.8]是什么,通过对源码进行分析,Node是HashMap的一个实现类,实现了Map.Entry接口,本质上还是一个key-value键值对,上图中的蓝色和黑色圆圈本质上都是一个Node对象。
    在这里插入图片描述

  • HashMap是使用哈希表来进行存储的,哈希表为了解决冲突,HashMap主要是采用了键地址法来解决问题,简单来说就是数组和链表的组合,在每个数组的后面都有一个链表,当数据通过hash算法得到一个值后,其对应的数据会放到一个对应的hash链表上进行存储,比如我们经常写的一种写法是map.put(“玉麒麟欧巴”,“Java开发程序员”);
    当我们进行相关的功能的开发的时候,我们通过对相关的key进行相关的Hashcode编码可以得到其相关的位置,相关的hash值后,我们就会进行相关的hash碰撞,当我们计算的值越分散,Hash的碰撞的几率就会少很多,map的取值率就会少很多。当然我们也并不是容器越大,其相关的碰撞率就一定会下降,此时我们要考虑的是有一个好的hash算法和一个扩容机制。

  • 在理解hash和扩容之前,我们首先要了解HashMap的几个字段,在只有hashMap对于的源码之前我们首先要了解几个字段,此时我们可以进行相关的功能的判断
    在这里插入图片描述
    Node[] table的默认值是length = 16,load factor为负载因子,默认值是0.75,threshold是所能容纳的最大的数据量的个数,显然是threshold = length * load factor,也就是在数组长度确定后,负载因子越大,其所能容纳的键值个数就越多。
    此时这里存在一个问题,即使负载因子和Hash算法设置的再合适,也难免避免不了拉链过长的情况,一旦拉链过长就会影响HashMap的性能,于是在Jdk1.8的时候引入了红黑树的数据结构,一旦链表的长度超过8,链表就会快速转换为红黑树,此时利用红黑树的快速的增删查改的速度,就能快速的提高其HashMap的运行的效率。

功能方法

HashMap的底层方法有很多,本文主要从怎么从key获取Hash桶的位置,put方法的详细执行,扩容三个有代表性的点进行详细的展开详解。

确定Hash桶数组索引位置
在我们在使用这个hashMap的时候,我们首先要做的就是确定hash数组的位置,正如我们所说,hash桶的结构是由数组+链表+红黑树组成的,我们进行hash算法的时候,我们当然希望其对于的hash值越分散越好,这样我们就能够通过hash算法快速定位到我们想要的值,而不用遍历链表,这样就能加快我们的开发的效率,那么我们先从源码中探索其结构,其过程就是三步:取key的hashCode值,高位运算,取模运算。
在这里插入图片描述
在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

下面举例说明下,n为table的长度。

在这里插入图片描述
hashMap的put方法

put方法的流程相对较复杂,下面这是我写的关于其实现的数据的过程,具体的流程如图所示,下面可以进行相关的分析。

在这里插入图片描述

  1. 判断table[i] == null是空或者是null,否则就是resize()方法进行扩容。
  2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向6,如果table[i]不为空,转向3;
  3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向4,这里的相同指的是hashCode以及equals;
  4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5;
  5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
  6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
   
        //tab:引用当前hashMap的散列表
        //p:表示当前散列表的元素
        //n:表示散列表数组的长度
        //i:表示路由寻址 结果
        Node<K,V>[] tab; Node<K,V> p; int n, i;

        //延迟初始化逻辑,第一次调用putVal时会初始化hashMap对象中的最耗费内存的散列表
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

        //最简单的一种情况:寻址找到的桶位 刚好是 null,这个时候,直接将当前k-v=>node 扔进去就可以了
        if ((p = tab[i = (n - 1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值