前言

          由于HashMap在我们的工作和面试中会经常遇到,所以搞懂HashMap的底层结构原理就显得十分有必要了。在JDK1.8之前,HashMap的底层采用的数据结构是数组+链表,而在JDK1.8及以后,HashMap的底层采用的数据结构是数组+链表+红黑树。因此想要弄懂HashMap的底层结构原理,需要先弄懂数组、链表、红黑树这三种数据结构。

一、数据结构之数组详解

数组定义:采用一段连续的存储单元来存储数据。(看图说话)

数组特点:   查询O(1),删除插入O(N)。这也就意味着数组查询快,插入慢。(这里O指的是时间复杂度,而评判一个算法的好坏主要是看时间复杂度和空间复杂度。)

可能有人会好奇这里的查询O(1)是什么意思?这个意味着这里的查询为常数级别的,其中O(1)中的1并不是说只查询一次,这个也是可能会查询2次、3次、多次的,这个1只是代表是常数。比如说下图,将数值2赋值给下标为4的整数数组,然后进行输出。那么程序只需要查询该数组下标为4的数据,然后便可以得到数值2了,这个就是时间复杂度为O(1)。

public class Array {// 数组: 采用一段连续的存储单元来存储数据// 特点: 指定下标O(1),删除插入O(N) 数组:查询快,插入慢public static void main(String[] args) {Integer integers[] = new Integer[10];integers[0] = 0;integers[1] = 1;integers[4] = 2;// 下面这行代码将会输出2System.out.println(integers[4]);}
}

而删除插入O(N)又是什么意思呢?假设现在有n个元素,而我现在要删除下标为2的值,那么之前下标为2后面的元素都需要依次向前移一位(看图说话)。如果删除的下标是0的元素,那么后面的元素都需要向前移动n次,那么时间复杂度是O(n)。如果删除的下标是n-1的元素,那么元素不需要移动,那么时间复杂度度是O(1),如果删除的元素是中间的,那么时间复杂度O(n/2)。而我们看时间复杂度一般也是以最坏的时间复杂度为标准的,所以说删除插入为O(n)。插入亦如此。

经典实现: Java里面的ArrayList就是再次基础上实现的。

二、数据结构之链表详解

        链表定义:链表是一种物理存储单元上非连续、非顺序的存储结构。(看图说话)

链表特点:  插入、删除时间复杂度O(1) ,查找遍历时间复杂度O(N)。 插入快,查找慢。

       这里可能有人会好奇什么说链表的插入、删除快,而查询慢呢?比如下面这个图,假设我需要将删除第二个元素,我只需要将next引用指向第三个元素就可以了。而插入的话,只需要插入元素的前一个元素的引用指向插入元素,同时插入元素的引用指向下一个元素,那么就可以插入了。这样看下来链表的插入、删除是不是很快呀,因为插入和删除只需要改变引用。

那么插入删除的时间复杂度为O(1)。而为什么又说查询很慢呢,因为是从头节点开始进行查询,然后看是否有查询的目标,没有就根据next引用继续往下查找,指导遍历找到为止。比如说我现在要查询下图的第一个链表里面的小胜,那么我就需要先从头节点开始查询,发现头节点只有小海而没有小胜,那么就继续根据next引用继续往下查询,结果发现只有小李而没有小胜,那么就继续根据next引用往下查询,最后发现了查询到了小胜,那么就返回结果。在这个过程中,其实前面两次查询是没有意义的,但是还是继续查询了,所以说链表的查询慢。如果链表有n个元素,那么最坏情况下查找的元素在最后一个,那么查询的时间复杂度就是O(n)。

       经典实现: Java里面的LinkList就是再次基础上实现的。

三、哈希(散列)算法详解

讲到这里在JDK1.8之前,HashMap的底层采用的数据结构是数组+链表也就讲完了,大家也对HashMap有了进一步了解了。但是HashMap的实现除了涉及到数据结构,其实还涉及到哈希算法(也叫散列)。

定义:哈希算法(也叫散列),也就是把任意长度值(Key)通过散列算法变换成固定长度的key(地址),通过这个地址进行访问的数据结构。它通过把关键码值映射到表中一个位置来访问记录,以加快擦好像的速度。

      这个定义什么意思呢?(看图说话)比如说将key为John Simth的数据通过哈希散算法存到到哈希表下标为152的位置上。

而这个时候恐怕大家又会很好奇这个值是如何算出来的了?这就涉及到了hashcode了,那么什么是hashcode呢?hashcode又是怎么算的呢?

hashcode定义:通过字符串算出它的ascii码,进行mod(取模),算出哈希表中的下标。注意:这里的取模多少,具体是跟数组的长度相关的。

这里就是对429进行取模10,从而得到9。大家现在明白了hashcode是如何算出来的了,恐怕就会问,那么这个ascii码值该如何得到了,这里提供一个程序。

/*** @Auther: limingwu* @Date: 2021/3/7 18:05* @Description:*/
public class AsciiCode {public static void main(String[] args) {char c[] = "lies".toCharArray();for (int i = 0; i < c.length; i++) {System.out.println((c[i]) + ":" + (int) c[i]);}}
}

讲到这里是不是觉得哈希(散列)算法很强大,但是这个算法也容易引发一个问题,那就是容易发生哈希冲突(碰撞)。这是什么意思呢?(看下图说话) 比如"lies"和"foes"两个字符串的ascii码值是一样的,那么     通过哈希算法的出来的hashcode值肯定也是一样的,所以但是他们在哈希表中的位置是一样的。但是同一位置又不能存储两个元素,那么怎么办呢?于是聪明的工程师想到了用链表这个结构来存储。首         先"lies"通过哈希算法存储到下标为9的数组里面去,然后“foes”进行存储的时候,只需要将“lies”的引用指向"foes"就好了。看到这里,大家也明白了JDK1.8之前的HashMap存储和数据结构了吧,它就是这       么存储的。

四、HashMap底层关键原理分析

再讲hashMap的底层关键原理分析之前,我们先来看一段代码。

/*** @Auther: limingwu* @Date: 2021/3/9 10:43* @Description:*/
public class App {public static void main(String[] args) {Map<String, String> map = new HashMap<>();map.put("张三", "张三");map.put("李四", "李四");map.put("王五", "王五");map.put("孙七", "孙七");map.put("小武", "阿武刚巴得");// 下面这行输出阿武刚巴得System.out.println(map.get("小武"));}
}

通过这段代码我们知道了,hashMap都是put存储键值对,get查询。那么问题来了,这个put是如何存储值的呢?这里我们可以通过模拟一个put方法来分析一下。

/*** @Auther: limingwu* @Date: 2021/3/9 10:43* @Description:*/
public class App {public static void main(String[] args) {App map = new App();map.put("张三", "张三");map.put("李四", "李四");map.put("王五", "王五");map.put("孙七", "孙七");map.put("小武", "阿武刚巴得");// 下面这行输出阿武刚巴得// System.out.println(map.get("小武"));}public void put(String key, String value) {System.out.printf("key:%s:::::::::::hash值:%s:::::::::::存储位置:%s\r\n", key, key.hashCode(), Math.abs(key.hashCode() % 15));}
}

程序执行完以后会输出以下数据

key:张三:::::::::::hash值:774889:::::::::::存储位置:4
key:李四:::::::::::hash值:842061:::::::::::存储位置:6
key:王五:::::::::::hash值:937065:::::::::::存储位置:0
key:孙七:::::::::::hash值:744906:::::::::::存储位置:6
key:小武:::::::::::hash值:758071:::::::::::存储位置:1

现在我们根据输出结果以及上面的存储代码来一步一步分析。(看下图说话)首先我们看到"张三"这个key通过哈希算法算出来的值是774889,存储的位置是数组下标为4的位置。那么就会将这个键值存储到下    表为4的数组里面,那么问题来了,这个是用字符串存储这个键值存储的呢还是用对象存储的了?答案是一般多个字符串的都会用对象来存储,这个对象我们可以看成它是一个Entry,这个Entry里面就存储我们的key和value以及存储一个hash和next。比如说下图的map会依次往下存储,"张三"就会存储到下标为4的数组里面,同时存储进来的还有value,hash,next。可能有人会好奇这个next是干嘛用的,其实这个next就是为了解决哈希碰撞而设计的,上面也提到过。现在开始存储"李四",也是一样的过程,李四会存储在数组下标为6的位置上。后面map也是按照这种形式存储,直到存储"孙七"这个key,就开始稍微不一样了。有啥不一样呢?因为"孙七"这个key,通过哈希算法算出来的值和”李四“通过哈希算法算出来的值是一样的,那么他们所存储在数组中的位置也是一样的。那么这个时候怎么办呢?如果这个时候”孙七“也存储在这个数组下标为6的位置上,就会将之前存储的"李四"给覆盖掉。

那么这里我能不能将这个给覆盖掉了,如果覆盖掉了的话,这个"李四"就无法查询了。很明显这就不符合hashMap的设计理念了,因为hashMap不同的key,其实是可以查询出来的。那既然这里这个"李四"不能被覆盖,那么这个地方该怎么办呢?(看图说话) 这个时候可以让之前存储下标一样的对象让一个位置,这里就是让"李四"让个位置。然后新存储的对象,也就是这里的”孙七“存储进来。最后让新进来的对象的next引用指向刚刚让出位置的对象,也就是说让"孙七"的next引用指向"李四"。这个地方其实就是一个链表结构了,这个也是为什么hashMap既用数组结构,又用链表结构的原因了。

存储完”孙七“,以后就是存储"小武"了,这个地方还是按照之前的方式存储。所以下图就是hashMap存储值的一个方式了。

现在讲完了hashMap存储(put),那么hashMap查询(get)是如何查询的呢?条条大路通罗马,其实hashMap查询和hashMap存储也是一样的道理。(看图说话)比如说我们现在要查询key为"李四"的值,那么我们就会通过hash算法得到数组下标为6,找到key为”孙七“的元素。这个时候会去比较key和hashCode,发现"李四"和”孙七“的key以及hashCode不一样,那么就会去看判断"孙七"有没有next引用,如果有next引用就取出来,然后判断key和hashCode值是否一样,如果一样那么就返回这个对象的value,如果不一样就返回null,这个就是我们查询(get)的一个逻辑。

上面的图是HashMap的一个查询和存储的思想,通过这个思想我们可以自定义属于我们的HashMap,以下就是自定义HashMap。

package com.awu.hashmap;public interface Map<K, V> {V put(K k, V v);V get(K k);int size();interface Entry<K, V> {K getKey();V getValue();}
}
package com.awu.hashmap;/*** @Auther: limingwu* @Date: 2021/3/9 16:18* @Description:*/
public class HashMap<K, V> implements Map<K, V> {private Entry<K, V>[] table = null;private int size = 0;public HashMap() {// 这里数组长度定义为16,是因为HashMap的默认容量就是16this.table = new Entry[16];}/*** 首先通过key进行hash,%数组下标长度得到对应下标Entry。然后判断是否为空,如果为空直接存放当前下标数组。如果不为空,* 判断next是否为空,如果next为空,直接赋值。如果不为空,再判断当前next是否为空,最后重复上述操作。** @param k* @param v* @return*/@Overridepublic V put(K k, V v) {int index = hash(k);Entry<K, V> entry = table[index];if (entry == null) {size++;table[index] = new Entry<>(k, v, index, null);} else {table[index] = new Entry<>(k, v, index, entry);}return table[index].getValue();}private int hash(K k) {int i = k.hashCode() % 16;return i >= 0 ? i : -i;}/*** 首先对Key进行hash,取得下标index。然后判断这个下标对应得Entry是否为空,如果为空,说明没有内容,直接返回null。如果不为空,* 比较查询出来得key和取出来得key是否相等,如果相等,直接返回该Entry。如果不相等,判断该Entry的next指向的Entry是否为空,* 如果为空,说明没有找到,直接返回null。如果不为空,比较查询出来得key和取出来得key是否相等,最后重复上述操作。** @param k* @return*/@Overridepublic V get(K k) {if (size == 0) {return null;}int index = hash(k);Entry<K, V> entry = findValue(table[index], k);return entry == null ? null : entry.getValue();}public Entry<K, V> findValue(Entry<K, V> entry, K k) {if (entry.getKey().equals(k) || k == entry.getKey()) {return entry;} else {if (entry.next != null) {findValue(entry.next, k);}}return null;}@Overridepublic int size() {return 0;}class Entry<K, V> implements Map.Entry<K, V> {K k;V v;int hash;Entry<K, V> next;public Entry(K k, V v, int hash, Entry<K, V> next) {this.k = k;this.v = v;this.hash = hash;this.next = next;}@Overridepublic K getKey() {return k;}@Overridepublic V getValue() {return v;}}
}
package com.awu.hashmap;/*** @Auther: limingwu* @Date: 2021/3/9 10:43* @Description:*/
public class App {public static void main(String[] args) {HashMap<String, String> map = new HashMap<String, String>();map.put("张三", "张三");map.put("李四", "李四");map.put("王五", "王五");map.put("孙七", "孙七");map.put("小武", "阿武刚巴得");// 下面这行输出阿武刚巴得System.out.println(map.get("小武"));}public void put(String key, String value) {System.out.printf("key:%s:::::::::::hash值:%s:::::::::::存储位置:%s\r\n", key, key.hashCode(), Math.abs(key.hashCode() % 15));}
}

这里我们自定义的HashMap也创建好了,并且运行正常。但是这里存在一个问题,由于我们的数组长为16,随着存储的数据量的增多,数组会先存满,然后剩余的数据只好存储到链表了,那么这个时候就会显得链表超级长。而链表太长了,就会导致我们的查询速度变慢。那这个地方如何解决了,Java是通过引入红黑树来提供查询效率的,众所周知红黑树的时间复杂度为O(lgn),而链表的时间复杂度为O(n),所以使用红黑数会比使用链表更加有效率。这个也是为什么JDK1.8及1.8以后HashMap的底层数据结构是由数组、链表、红黑树组成的原因了。那么这里可能会有人好奇了,既然红黑树比链表更加有效率,为啥还用链表呢?这是因为黑红树在插入的时候,需要去维护“小、中、大”,“左、根、右”的这样的一个结构,也就是树的旋转。所以Java里面根据大量实验证明,当数据长度小于等于7的时候用链表,当大于7的时候用红黑树,这也是最有效率的。下图是Java HashMap里的源码,可以看见是和我们说的是一样的。

到此,集合之HashMap底层结构原理讲完,觉得有帮助的给个赞吧,谢谢

聊聊Java系列-集合之HashMap底层结构原理相关推荐

  1. java源码系列:HashMap底层存储原理详解——4、技术本质-原理过程-算法-取模具体解决什么问题

    目录 简介 取模具体解决什么问题? 通过数组特性,推导ascii码计算出来的下标值,创建数组非常占用空间 取模,可保证下标,在HashMap默认创建下标之内 简介 上一篇文章,我们讲到 哈希算法.哈希 ...

  2. HashMap底层实现原理,红黑树,B+树,B树的结构原理,volatile关键字,CAS(比较与交换)实现原理

    HashMap底层实现原理,红黑树,B+树,B树的结构原理,volatile关键字,CAS(比较与交换)实现原理 首先HashMap是Map的一个实现类,而Map存储形式是键值对(key,value) ...

  3. Java中HashMap底层实现原理

    Java面试绕不开的问题: Java中HashMap底层实现原理(JDK1.8)源码分析 这几天学习了HashMap的底层实现,但是发现好几个版本的,代码不一,而且看了Android包的HashMap ...

  4. HashMap底层实现原理/HashMap与HashTable区别/HashMap与HashSet区别(转)

    HashMap底层实现原理/HashMap与HashTable区别/HashMap与HashSet区别 文章来源:http://www.cnblogs.com/beatIteWeNerverGiveU ...

  5. HashMap底层实现原理--详细

    参考链接: Java集合 - (源码解析)"HashMap底层实现原理–详细" 为什么面试要问 hashmap 的原理

  6. JDK1.7中HashMap底层实现原理

    JDK1.7中HashMap底层实现原理 一.数据结构 HashMap中的数据结构是数组+单链表的组合,以键值对(key-value)的形式存储元素的,通过put()和get()方法储存和获取对象. ...

  7. Java面试绕不开的问题: Java中HashMap底层实现原理(JDK1.8)源码分析

    这几天学习了HashMap的底层实现,但是发现好几个版本的,代码不一,而且看了Android包的HashMap和JDK中的HashMap的也不是一样,原来他们没有指定JDK版本,很多文章都是旧版本JD ...

  8. 【java】HashMap底层实现原理及面试题

    目录 一.哈希表(散列) 1.什么是哈希表 2.什么是哈希冲突(面试题) 3.解决哈希冲突的方法(面试题) (1) 开放地址法 ① 线性探查 ②二次探查 ③随机探查 (2) 再哈希法 (3) 链地址法 ...

  9. 【Java Map集合 之 hashMap工作常用遍历操作】

    集合关系图 1.文章前介 日常工作中常用的集合有ArrayList.HashMap和HashSet.前两者在开发中更是广为使用.本章主要介绍的是Map下HashMap 在日常工作中的遍历操作.将会以容 ...

最新文章

  1. linux进程间通信:消息队列实现双端通信
  2. Chrome v28 会在pwd目录下生成libpeerconnection.log文件
  3. 3.运算符与表达式,控制流
  4. 使用Visual Studio 2017创建React项目
  5. python 封闭图形面积_python实现计算图形面积
  6. SylixOS armv8 任务切换
  7. 超详细Eclipse安装教程
  8. Delphi第三方控件大比拼(收费篇)
  9. linux服务器根据requestId查看日志
  10. 分享解决方法:为什么QQ聊天框中无法使用输入法输入中文?
  11. 东南大学计算机学院足球队,2017春季“放飞智能”杯东南大学苏州校友足球队比赛赛事系列报道(八)...
  12. Linux中关于一个文件的详细信息
  13. 拯救纠结症 选iPhone SE还是iPhone6?
  14. PostgreSQL问题解决--连接数过多
  15. 智能摄像头雷达感应技术,雷达传感器模组应用,家居智能化发展
  16. 【python拼图】遍历文件夹后,自动拼接图像成正方形图,或者指定行数显示
  17. 手机php网站开发工具,4款好用的网站开发工具推荐
  18. FC游戏修改教程(hack)小白文。
  19. python基础讲解:代码规范判断语句循环语句
  20. 广域网云主机或服务器_局域网或广域网主机的ip地址

热门文章

  1. BiLiBiLi爬虫
  2. 简单快捷通用导出word功能
  3. StarUml----正向工程操作步骤
  4. 怎么用计算机远程vdi,windows8远程桌面虚拟机配置以便支持VDI用户的访问
  5. 【代码随想录】LC 102. 二叉树的层序遍历
  6. GET http://localhost:18086/css/all.css:浏览器无法访问项目下的静态资源
  7. (17)网络安全:cookie注入、二次注入、DNSlog注入、中转注入、堆叠注入的原理及注入过程
  8. 〖Python零基础入门篇(58)〗- Python中的虚拟环境
  9. 从NEO源码分析看DBFT共识协议
  10. 智能车-一年总结(十六届)