注意以下文章可能有描述和理解上的错误,如果出现错误请到评论区指出,我会第一时间修改问题。也希望文章能解决你的疑惑。

HashMap结构图

HashMap底层数据结构:Entry数组+链表+红黑树(JDK1.8版本) Entry+链表(JDK1.7版本)

这里写目录标题

  • HashMap结构图
  • 代码分析
    • 常见的参数及意义
    • 源码解释
      • 构造方法
      • size函数
      • isEmpty函数
      • get具体过程函数
      • containsKey函数
      • put函数
      • resize函数
      • remove函数
      • clear函数
      • containsValue函数
      • keySet函数
      • values函数
      • entrySet函数
    • 面试常见的问题

代码分析

常见的参数及意义

 //默认的Hash表的长度static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16//Hash表的最大长度static final int MAXIMUM_CAPACITY = 1 << 30;//默认加载因子static final float DEFAULT_LOAD_FACTOR = 0.75f;//当链表的长度为8的时候转化为红黑树static final int TREEIFY_THRESHOLD = 8;//桶中元素个数小于6的时候红黑树转换为链表static final int UNTREEIFY_THRESHOLD = 6;//只有当数组的长度大于等于64并且链表个数大于8才会转换为红黑树static final int MIN_TREEIFY_CAPACITY = 64;//Hash表transient Node<K,V>[] table;//遍历的时候使用返回一个K-V集合transient Set<Map.Entry<K,V>> entrySet;//表中K-V的个数transient int size;//对集合的修改次数,主要是后面出现的集合校验transient int modCount;//阈值当size大于threshold时就会进行resizeint threshold;//加载因子final float loadFactor;

源码解释

构造方法

//传入初始化容量,和指定的加载因子public HashMap(int initialCapacity, float loadFactor) {//参数校验if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);//如果传入的值大于最大容量,就将最大的值赋给他if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;//参数校验if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;//返回的是2的整数次幂this.threshold = tableSizeFor(initialCapacity);}//指定HashMap的容量public HashMap(int initialCapacity) {//调用如上的双参构造函数this(initialCapacity, DEFAULT_LOAD_FACTOR);}//无参构造函数public HashMap() {//初始化加载因子为默认的加载因子this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}//构造一个映射关系与指定 Map 相同的新 HashMap。public HashMap(Map<? extends K, ? extends V> m) {//初始化加载因子为默认的加载因子this.loadFactor = DEFAULT_LOAD_FACTOR;//构造的过程函数putMapEntries(m, false);}final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {//获取m集合中元素个数int s = m.size();//如果m集合元素个数是0个那么下面这些操作也就没有必要了if (s > 0) {if (table == null) { //表示的拷贝构造函数调用putMapEntries函数,或者是构造了HashMap但是还没有存放元素//计算的值存在小数所以+1.0F向上取整float ft = ((float)s / loadFactor) + 1.0F;//将ft强制转换为整形int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);//如果计算出来的值大于当前HashMap的阈值更新新的阈值为2次方if (t > threshold)threshold = tableSizeFor(t);}else if (s > threshold)//如果Map集合元素大于当前集合HashMap的阈值则进行扩容resize();//将Map集合中元素存放到当前集合中for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {K key = e.getKey();V value = e.getValue();putVal(hash(key), key, value, false, evict);}}}

size函数

 //返回key-val的数量public int size() {return size;}

isEmpty函数

   //当前的集合是否为nullpublic boolean isEmpty() {return size == 0;}

get具体过程函数

//根据key获取对应的valpublic V get(Object key) {Node<K,V> e;//通过hash值,key找到目标节点再返回对应的valreturn (e = getNode(hash(key), key)) == null ? null : e.value;}//获取key对应的节点  final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;//如果集合为空和对应的下标数组中的值为空直接返回null//first = tab[(n - 1) & hash]数组的长度是2n次方减1后对应位全部变为1,这样为与操作永远都会在数组下标范围内不会越界if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {if (first.hash == hash && // 如果第一个节点hash与对应hash相等,并且key也相等则返回当前节点((k = first.key) == key || (key != null && key.equals(k))))return first;//第一个节点的下一个节点不为nullif ((e = first.next) != null) {//判断节点是否为树形if (first instanceof TreeNode)//在树形结构中查找节点并返回return ((TreeNode<K,V>)first).getTreeNode(hash, key);do {//通过do...while结构遍历找对应key的节点if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))//找到节点并返回return e;} while ((e = e.next) != null);}}//未找到对应的节点return null;}

containsKey函数

 //查看是否包含指定key    public boolean containsKey(Object key) {//通过getNode返回是否为null判断是否存在keyreturn getNode(hash(key), key) != null;}

put函数

在此之前先看一下put的过程

//调用putVal向当前集合中存放元素并返回对应的valpublic V put(K key, V value) {return putVal(hash(key), key, value, false, true);}//存放对应的key-valfinal V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;//如果当前集合为null则将集合扩容并且将新的存放结构赋值给tabif ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;//找到key存放的链表,如果为空直接将当前节点存放链表在第一个位置if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else { //当前为链表不为nullNode<K,V> e; K k;//表示当前链表第一个位置key已经存在,将当前节点赋值给eif (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//查看当前的节点是否属于树形结构如果是则在TreeNode中查找并将赋值给eelse if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {//找到当前存放位置节点的最后一个节点的next并将当前要插入的节点插入if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // 链表的长度为8的时候转化为红黑树减一是因为元素从0开始treeifyBin(tab, hash);//跳出死循环break;}//表示的是当前链表已经存在当前要插入的key,HashMap不存在重复的keyif (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;//将节点后移p = e;}}if (e != null) { // 当前节点不为null将e.val存放在oldValueV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)//不管oldValue是否为null都会发生value赋值给e.value//当出现重复的key之后上面会将节点保存给e并未修改新的val值,在此更新e.value = value;//将结点向后调整到最后面afterNodeAccess(e);//如果为null返回null,不为null返回对应的valreturn oldValue;}}//++modCount对其集合操作的次数+1++modCount;if (++size > threshold)//如果在放入元素以后大于阈值则进行2倍扩容resize();afterNodeInsertion(evict);return null;}

resize函数

 //将集合扩容final Node<K,V>[] resize() {Node<K,V>[] 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;//将整型最大的值赋值给thresholdreturn oldTab;}//当前集合数组长度扩大二倍赋值给newCap小于MAXIMUM_CAPACITY//并且集合的容量大于等于默认容量将当前阈值扩大二倍赋值给新的阈值 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // double threshold}//若没有经历过初始化,通过构造函数指定了initialCapcity,将当前容量设置为大于它最小的2的n次方else if (oldThr > 0) newCap = oldThr;else {               // 初始的时候长度和阈值都使用默认值newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}//重新计算thresholdif (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}//更新当前集合阈值threshold = newThr;//从这里开始便是将oldTab数据重新hash放入扩容后的newTab@SuppressWarnings({"rawtypes","unchecked"})Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//将table指向的oldTab指向newTabtable = newTab;if (oldTab != null) {//遍历哈希表for (int j = 0; j < oldCap; ++j) {Node<K,V> e;//当前链表是否为null、并且将就链表赋值给eif ((e = oldTab[j]) != null) {oldTab[j] = null;//将原来位置的链表置为null方便垃圾回收if (e.next == null)//链表的长度为1直接将链表中的一个节点重新hash存放到相应的位置newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode) //表示节点类型为树形结构((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { //链表是非树形结构,并且节点数量是大于1//将链表拆分为两个子链表Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {  //通过do...while遍历链表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;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {//在新表的j位置存放链表loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {//在新表的j+oldCap位置存放链表hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}

remove函数

     // 移除指向key返回对应的val          public V remove(Object key) {Node<K,V> e;//返回如果为空返回null否则返回e.valreturn (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;}final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {Node<K,V>[] tab; Node<K,V> p; int n, index;//常规的判断表不为null,key有对应的存储位置if ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {Node<K,V> node = null, e; K k; V v;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))//表示的是key存储在当前链表的第一个位置node = p;else if ((e = p.next) != null) {//表示的是链表的长度大于1if (p instanceof TreeNode)//判断是否是树的实列//返回对应key在红黑树存储的位置node = ((TreeNode<K,V>)p).getTreeNode(hash, key);else {//当前结构为链表do {//遍历链表if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {//找到对应的节点保存并跳出循环node = e;break;}//将节点后移p = e;} while ((e = e.next) != null);}}//表示要删除的key存在并且找到if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {if (node instanceof TreeNode)//如果是树形在树型结构中移除当前节点((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);else if (node == p)//表示的链表中的第一个节点tab[index] = node.next;elsep.next = node.next;//移除节点++modCount;//操作+1--size;//长度-1afterNodeRemoval(node);//返回节点return node;}}return null;}

clear函数

//清除集合中的所有key-value      public void clear() {Node<K,V>[] tab;//集合操作+1modCount++;if ((tab = table) != null && size > 0) {//表不为null才进行遍历size = 0;for (int i = 0; i < tab.length; ++i)//遍历集合所有元素都置为null,方便垃圾回收tab[i] = null;}}

containsValue函数

     //查看集合是否包含指定valuepublic boolean containsValue(Object value) {Node<K,V>[] tab; V v;if ((tab = table) != null && size > 0) {//表不为nullfor (int i = 0; i < tab.length; ++i) {//遍历数组for (Node<K,V> e = tab[i]; e != null; e = e.next) {//遍历链表if ((v = e.value) == value ||(value != null && value.equals(v)))//存在指定的value直接返回truereturn true;}}}//集合中不存在指定value返回falsereturn false;}

keySet函数

 //返回key的所有集合setpublic Set<K> keySet() {Set<K> ks = keySet;if (ks == null) {ks = new KeySet();keySet = ks;}return ks;}

values函数

 //返回所有的value集合    public Collection<V> values() {Collection<V> vs = values;if (vs == null) {vs = new Values();values = vs;}return vs;}

entrySet函数

   // 返回所有的key-value集合public Set<Map.Entry<K,V>> entrySet() {Set<Map.Entry<K,V>> es;return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;}

面试常见的问题

  1. 为什么HashMap默认的长度为2的整数次幂?

    就是因为获取索引h&(length-1)可以保证散列的均匀,避免不必要的hash冲突。

  2. 为什么加载因子是0.75?大了会怎么样?小了会怎么样?

    首先加载因子是表示hash表的填满程度,当为0.75的时候是在对提高空间利用率和减少查询成本的折中,当大于0.75的时候填满的元素越多,空间利用率越高,但是冲突的概率变大;当小于0.75的时候填满的元素越少,空间利用率越低,但是冲突的概率变小。

  3. 什么是哈希冲突?如何解决?

    哈希冲突是指hash出来的地址被其他元素所占用;
    解决的方法
    1.链地址法
    解决的思路就是当出现冲突的时候将冲突的元素加入当前的链表之中

    2.开放地址法
    开放地址法也称之为再散列。
    思路:如果映射的地址被占用了,在哈希函数值的基础上加上指定数值,这样就可以把冲突的地址给错开,然后重新开辟新的地址用来存储。根据增量值的不同,分为线性探测再散列和二次探测再散列

    3.再哈希法
    这种方法就是构造多个不同的哈希函数,当哈希地址Hi=RH1(Key)发生冲突时,再计算Hi=RH2(Key)…直到哈希不冲突,这样的方法增加了计算的时间。
    4.建立公共溢区
    就是哈希表分成了两个表:一个是基础表,另外一个则是溢出表,凡是与基础表发生冲突的数据都会被添加到溢出表。

  4. 什么是扰动函数?怎么设计的?为什么这个设计?

    扰动函数是hash函数拿到k的hashcode值,这个值是一个32位的int,让高16位与低16位进行异或。
    理论上来说字符串的hashCode是一个int类型值,那可以直接作为数组下标了,且不会出现碰撞。但是这个hashCode的取值范围是[-2147483648, 2147483647],有将近40亿的长度,谁也不能把数组初始化的这么大,内存也是放不下的。
    混合原始哈希码的高位和低位,以此来加大低位的随机性,这样设计在一定的程度上减少了hash碰撞,优化了散列的效果 。

  5. JDK1.8在对HashMap较1.7有什么优化?

    1.首先是最重要的就是底层的数据结构,1.7的时候底层数据结构是数组+链表;而在1.8的时候变成了数组+链表+红黑树
    2.在哈希上1.7扰动四次,1.8做了一次扰动,可以提高效率
    3.1.7在进行resize扩容的时候是重新哈希,1.8的时候采用的是索引位置不变或者就是就哈希表的容量+当前索引。
    4.1.7采用插入方式是头插法,1.8采用的是尾插法。

  6. 为什么1.8扩容不用重新哈希?

  7. HashMap线程安全吗?为什么不安全?怎么解决不安全?

    首先HashMap是线程不安全的。JDK1.7的时候采用头插法,多线程同时插入的时候,A线程在插入节点B,B线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,后遍历到的B节点放入了头部,这样形成了环。JDK1.8采用尾插法,会造成两种情况两个线程同时插入只有一个成功插入,还有就是可能会造成两次resize(++size > threshold) 。解决的方案:一、使用HashTable效率比较差。二、使用ConcurrentHashMap比较常用的。三、使用Collections.synchronizedMap() 以上三种线程安全。

  8. HashMap内部节点是有序的吗?

    不是有序的。有序的Map集合有LinkedHashMap、TreeMap

  9. HashMap一般采用什么作为key?

    HashMap一般采用String、Integer 等类作为key、因为这些类底层已经重写了hashcode、equals方法,用的是final修饰类在多线程情况下相对安全。

  10. 为什么重写equals还要重写hashcode?

    比如HashMap中不允许存在相同的key,当重写了equals方法没有重写hashcode方法,当两个对象中的值相同,但是他们hashcode不同会造成比如
    class newInstance1 = new class(1);
    class newInstabce2 = new class(2);
    以上的比较对象的时候hashcode不同,equal方法比较返回false;但是重写Hashcode后,可以达到返回true。

如果看完觉得得到帮助就留下你的一键三连,谢谢 注意如果有错误的地方评论区提出来我立即更正谢谢大佬的指正。

HashMap原理底层剖析相关推荐

  1. hashmap原理_HashMap和HashTable底层原理以及区别

    HashMap底层原理 哈希表:在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1). 数据结构的物理存储结构只有两种:顺序存储结构和 ...

  2. Java基础-hashMap原理剖析

    Java基础-hashMap原理剖析 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.   一.什么是哈希(Hash) 答:Hash就是散列,即把对象打散.举个例子,有100000条数 ...

  3. 为什么要学习HashMap的底层原理?

    本文转载自公众号  码农翻身 上周发了一篇文章<漫画:什么是HashMap?>,引起了不少人的讨论,有一个人的留言引发了我的思考:"作为一个程序员, 真的有必要学习这些底层原理吗 ...

  4. 复习一波,hashMap的底层实现原理

    前言 HashMa是Java中最常用的集合类框架,也是Java语言中非常典型的数据结构,同时也是我们需要掌握的数据结构: java中集合的分类: java中的集合可以分为:单列集合(collectio ...

  5. HashMap的底层原理你真的知道?

    HashMap的底层实现原理是面试中出现频率非常高的一道面试题,本文将对HashMap的底层实现原理做一个简要的概况和总结,便于复习. 一.对于Map集合存储结构的理解 首先介绍以HashMap为典型 ...

  6. HashMap 的底层原理

    HashMap 的底层原理 1. HashMap的数据结构 数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端. 数组 数组存储区间是连续的,占用内存严重,故空间复杂的很大.但数组的二 ...

  7. 为什么使用HashMap需要重写hashcode和equals方法_最通俗易懂搞定HashMap的底层原理...

    HashMap的底层原理面试必考题. 为什么面试官如此青睐这道题? HashMap里面涉及了很多的知识点,可以比较全面考察面试者的基本功,想要拿到一个好offer,这是一个迈不过的坎,接下来我用最通俗 ...

  8. HashMap的底层存储结构和实现原理

    文章目录 前言 一.HashMap是什么? 二. 数组 三. 链表 四.哈希算法 五.哈希冲突 总结 前言 HashMap实现了Map接口,我们常用来put/get操作读存键值对数据,比较典型的key ...

  9. java集合:HashMap的底层实现原理

    HashMap的底层实现原理是面试中出现频率非常高的一道面试题,本文将对HashMap的底层实现原理做一个简要的概况和总结,便于复习. 一.对于Map集合存储结构的理解 首先介绍以HashMap为典型 ...

最新文章

  1. 猎八哥浅谈存储过程——数据库中的双刃剑
  2. 两个有序数组的中位数 python_Python寻找两个有序数组的中位数实例详解
  3. 【Android】Camera 使用浅析
  4. HTML span标签学习笔记
  5. linux中网页播放音乐,Linux_在Linux系统下播放网页中的背景音乐技巧,在Linux中的firefox浏览许多网页 - phpStudy...
  6. 算法入门篇三 详解桶排序和整理排序知识 堆的相关操作 补充 不完整
  7. 融资2.5亿的国产浏览器,被曝只是打包chrome
  8. Nest入门教程 - 初识控制器
  9. 下半年的电商促销决战,设计师美工准备好了么?宝藏模板拿走!
  10. 论一切都是文件之匿名 inode
  11. 放在请求头目的_YSLOW性能测试前端调优23大规则(三)添加Expires头
  12. 订阅号获取openid_小程序订阅消息
  13. 房地产企业与项目管理
  14. 王建农老师昆笛 + 简谱
  15. 基于springboot 支付宝app端支付,可用于uni-app使用
  16. 怎样调整计算机视角,电脑调节不了CAD极轴角度怎样解决|电脑中调节CAD极轴角度的方法...
  17. 03-白龙马与拉磨驴的人生
  18. 【C++算法题】求三角形边长
  19. 1026 Table Tennis (30 分)模拟排列问题
  20. 如何把一个程序封装成一个系统?(1)

热门文章

  1. 【Linux成长之路】CentOS7查看Tomcat版本号的方法
  2. Anaconda添加channels
  3. mysql btree索引原理_Postgres BTREE索引原理简单介绍
  4. 图自监督学习在腾讯Angel Graph中的实践
  5. MarkDown标准格式
  6. MySQL的存储引擎
  7. 【记录我的作业2】工业机器人职业技能训练——week3RobotStudio纪念币和键盘工作台仿真。
  8. 知识图谱-汽车品牌知识图谱实战复现记录
  9. 2022 年,中国光伏走入 “ 平价时代 “
  10. 定义一个中国银行类,属性:账号、密码、余额,银行名称, 方法:注册账号,存款、取款、查询余额。创建该类对象并测试