文章目录

  • 前言
  • 一、HashMap是什么?
  • 二、 数组
  • 三、 链表
  • 四、哈希算法
  • 五、哈希冲突
  • 总结

前言

HashMap实现了Map接口,我们常用来put/get操作读存键值对数据,比较典型的key-value结构,那么本文将详细分析此数据结构的底层原理及实现,包括底层存储原理,哈希算法,哈希冲突,源码等等。

一、HashMap是什么?

HashMap实际上是一种数组+链表(jdk1.8后加入红黑树)的一种数据结构,在put操作中通过内部定义的算法(哈希算法)找到数组的下标,将数组放在此数组元素中,如果该数组元素已经有了元素(hash冲突),将会把这个数组元素上的链表进行遍历,放在此链表的末尾,内部有next code下一步节点,根据这个来定义位置。

二、 数组

简单的说:数组就是采用一段连续的存储单元来存储数据。 特点:查询快O(1),删除插入慢O(N)
查询快:因为数组是有自增的下标(索引),查询的时候可以依据索引直接定位到要查询的数据,因此从查询的角度来说效率更高。
删除插入慢:因为:当插入或者删除数据的时候,其他的数据会重新计算下标,重新移位,时间复杂度就是O(N),假设有一个大小为10的数组,当要在下标4~5之间插入数据时,新数据的下标就为6,而原本为6的数据下标则为7,以此类推在6后面的所有数据都要重新移位,计算下标,除非插入和删除的是最后一个数据。

自己画的一个潦草的图

实际上ArrayList底层就是一个数组实现。

private static final long serialVersionUID = 8683452581122892189L;
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
transient Object[] elementData; // 数组
private int size;
private static final int MAX_ARRAY_SIZE = 2147483639;/** arraylist的add方法 **/
public boolean add(E var1) {this.ensureCapacityInternal(this.size + 1);this.elementData[this.size++] = var1;return true;
}

上面代码是jdk1.8的源码,可以看到“elementData”实际上就是一个数组,所以arrayList的底层实现实际上也是一个数组,也具有查询快,删除插入慢的特性。

三、 链表

是一种根据元素节点逻辑关系排列的一种数据结构,可以保存多个数据,有点像数组的概念,但是数组有个一个缺点——数组的长度 是固定的,不可改变,在长度固定的情况下首选肯定是数组,但是在开发中有时候保存的内容长度是不确定的,这个时候就能用链表来代替数组使用。

/** 每一个链表实际上就是多个节点 **/
class Node {    /** 保存数据 **/private String data; /** 保存下一个节点 **/private Node nextCode;/** 每一个Node都必须保存有数据 **/public Node(String data){  this.data = data ;}public void setNextCode(Node nextCode){this.nextCode = nextCode ;}public Node getNextCode(){return this.nextCode ;}public String getData(){return this.data ;}
}public class LinkedList {public static void main(String[] args) {/** 1.准备数据 **/Node root = new Node("飞机头") ;Node n1 = new Node("机厢A") ;Node n2 = new Node("机厢B") ;/** 链接节点 **/root.setNextCode(n1);n1.setNextCode(n2);/** 2.取出数据 **/Node currentNode = root ;  //从当前根节点开始while( currentNode !=  null){System.out.println(currentNode.getData()) ;/** 将下一个节点设置为当前节点 **/currentNode = currentNode.getNextCode() ;}}
}

四、哈希算法

哈希算法是HashMap的底层实现原理里面很重要的一个组成部分,它来决定数据的位置及访问记录,哈希算法也叫散列算法,把我们存在map中的key(任意长度的值)通过此算法变成固定长度的key(地址),通过这个地址访问数据。

通过一张图,我们可以简单看一下

上图主要展示的就是我们存储一个任意长度的key,通过算法转换为一个固定地址。

实际上,hashCode是通过字符串算出它的ascii码,然后进行取模,得到哈希表的下标。


借用上面这张图,我们可以看到“lies”是存储到map里面的key,那么它是由4个字母组成,这4个字母分别的ascii码是l=108 i=105 e=101 s=115,相加等于429,那么这个429就是可以理解为lies的标识,然后通过hsah function(取模)得到哈希表的下标为9,把这个数据存储到下标为9的里面。

之所以要去hash function,主要是为了节省空间,这个也跟上面我所说到的数组的特性有关,假设lies的标识为429,那么数组是有固定长度的,不可能把数组扩容到429,那么这个时候就用到取模。

那么这个时候问题来了!!!敲黑板!!!
取模是为了节省空间,也是为了计算出在哈希表的下标,那么如果相同的ascii值去取模,得到的下标就是相同的,那么一个下标怎么存储两个数据呢?这个就叫哈希冲突(哈希碰撞)

五、哈希冲突

话不多说,先来张图简单看下

借用上面这张图,我们可以大概了解到哈希冲突的意思,“lies”与“foes”的ascii码都是429,那么相同的ascii码去取模,得到的哈希表下标都是9,这个时候就出现了哈希冲突。

那么出现哈希冲突后,不代表不能存储值或者覆盖原先的值,这个时候我们就用到了上面说的链表引入,因为链表是具有下一步指针特性的,当出现哈希冲突的时候,底层特性会将“lies”的下一步节点指向“foes”,那么也就是说在下标为9的结构里面,存储了lies,但是因为链表特性,lies的下一步节点指向了foes,

 public static void main(String[] args) {TestEs map = new TestEs();map.put("A", "A");map.put("B", "B");map.put("C", "C");map.put("D", "D");map.put("E", "E");}public void put(String key, String value) {logger.info("key={},hash值={},存储位置={}", key, key.hashCode(), Math.abs(key.hashCode() % 15));}put - key=A,hash值=65,存储位置=5
put - key=B,hash值=66,存储位置=6
put - key=C,hash值=67,存储位置=7
put - key=D,hash值=68,存储位置=8
put - key=E,hash值=69,存储位置=9

上述代码是我写的一个小例子,我进行取模取的是15,通过这个例子可以看到不管运行几次,hash值是不会变的;这也是hash算法的一个特点,是具有幂等性的。

这个时候HashMap的基本组成我想应该有个大概的了解了,那么我们使用PUT操作时,究竟是怎么一步步将值存储的呢?又是怎么样的流程呢?这个时候就要解析源码了。

JDK的源码我就不一一截图了,我仿造着源码自己写了套HashMap的实现,并把自己的理解都标注了注释。

首先定义一个Map接口
这个接口我会定义put/get方法 以及存储的接口

package com.company;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();}
}

创建一个实体类,实现定义的Map接口

package com.company;public class HashMap<K, V> implements Map<K, V> {private Entry<K, V> table[] = null;int size = 0;public HashMap() {this.table = new Entry[16];}/*** step1:通过key 使用hash算法得到hashCode值* step2:通过hash值取模得到index下标,找到此下标对应的对象* step3:判断当前对象是否为空  为空 可以直接存储* step4:不为空 说明出现了hash冲突 用到next链表特性* step5:返回当前节点对象** @param k* @param v* @return*/public V put(K k, V v) {/* 通过hash算法  得到要存储数组的下标 */int index = hash(k);/* 以此下标 获取数组中的对象 */Entry<K, V> entry = table[index];/* 为空 则证明可以直接进行存储 */if (null == entry) {table[index] = new Entry(k, v, index, null);size++;}/* 不为空,发生了冲突,使用链表 */else {table[index] = new Entry(k, v, index, entry);}return table[index].getValue();}/*** 这个就是我自己定义hash算法的方法* 这个比较简单,真实的源码会有一些移位的操作* 主要涉及到性能和计算机底层原理的一些东西 这个我还不懂** @param k* @return*/private int hash(K k) {int index = k.hashCode() % 16;return index >= 0 ? index : -index;}/*** 通过key 进行hash得到 index* 通过index下标获取数组对象* 判断当前对象是否为空 如果不为空* 判断是否相等 如果不相等* 判断next节点是否为空  如果不为空* 判断是否相等 直到相等或者为空。* 返回。** @param k* @return*/public V get(K k) {if (0 == size) {return null;}int index = hash(k);/* 以此下标 获取数组中的对象 */Entry<K, V> entry = findValue(table[index], k);return null == entry ? null : entry.getValue();}/*** 采取递归来完成get的步骤** @param entry* @param k* @return*/private Entry<K, V> findValue(Entry<K, V> entry, K k) {if (k.equals(entry.getKey()) || k == entry.getKey()) {return entry;} else {if (null != entry.next) {return findValue(entry.next, k);}}return null;}public int size() {return size;}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;}public K getKey() {return k;}public V getValue() {return v;}}
}

测试

package com.company;public class TestMap {public static void main(String[] args) {Map<String, String> map = new HashMap<String, String>();map.put("知夏", "这是一篇技术分享的文章");map.put("爱吃饭的侯", "小侯爱吃饭");System.out.println(map.get("知夏"));System.out.println(map.get("爱吃饭的侯"));}
}

这是参照源码简单写的一个map实现,代码解释,实现原理都写成了注释,实际的源码比这个发杂的多的多,但是这个例子依然是map的实现逻辑,当然这个例子没有实现红黑树。

这里顺便提一下红黑树,简单的说 当数组的容量固定的时候,如果存储的数据超过了数组的固定容量,那么就会出现巨大的哈希冲突,这个时候会使用链表特性,会纵向的无限延申数据,上面我也记录了链表的特性,插入快,查询慢,如果出现了这种情况 对性能和查询是非常不利的,这就是为什么要推出红黑树,红黑树准备再开一篇文章单独记录。


总结

写到这里,我们知道了HashMap的结构和底层实现,我们知道它是一种以数组+链表+红黑树为组合的key-value存储的数据结构,它是我们开发中使用率很高的一个朋友,它的底层涵盖了哈希算法,如何解决哈希冲突,如何解决链表无限延申,对数据和结构进行了重新定义,以前呢,我没有想过要去深入了解它,会用就行了,趁着疫情封控在家,花时间整理一下,顺带记录下来,回头忘了可以过来重温下。

HashMap的底层存储结构和实现原理相关推荐

  1. Git 底层数据存储结构与工作原理介绍

    前言 通过对 Git 底层 API 的使用来了解其存储结构与工作原理,通过了解工作原理可以帮助我们更好地解决各类 Git 代码版本管理操作上的问题. 介绍 Git 是一个内容寻址(content-ad ...

  2. redis底层数据结构(redis底层存储结构、源码分析)

    文章目录 前言 一.redis为什么快? 二.redis的底层数据结构 2.1.redis的底层存储的扩容机制 2.1.1.扩容时间 2.1.2.扩容多大 2.1.3.扩容后的rehash 2.1.4 ...

  3. 图解图库JanusGraph系列-一文知晓“图数据“底层存储结构(JanusGraph data model)

    图解图库JanusGraph系列-一文知晓"图数据"底层存储结构(JanusGraph data model) 大家好,我是洋仔,JanusGraph图解系列文章,实时更新 图数据 ...

  4. java对象底层原存储结构图解_图解图库JanusGraph系列-一文知晓“图数据“底层存储结构...

    大家好,我是洋仔,JanusGraph图解系列文章,实时更新~ 图数据库文章总目录: 转载文章请保留以下声明: 一:存储模式 留言或私信我,邀请你加入"图数据库交流"微信群! 1. ...

  5. janusgraph整合mysql_图解图库JanusGraph系列-一文知晓“图数据“底层存储结构

    527-7.jpg 大家好,我是洋仔,JanusGraph图解系列文章,实时更新~ 图数据库文章总目录: 版本:JanusGraph-0.5.2 转载文章请保留以下声明: 一:存储模式 留言或私信我, ...

  6. 二叉树存储结构 mysql_为什么mysql索引选择b+树作为底层存储结构?

    这篇文章解决一个问题mysql 底层为什么是用b+树作为存储结构?为什么不是二叉树,红黑树,b树? 我们先构造一个应用场景,我们有1kw的数据需要存储在一张表里面,那么我们怎么设计能让查询速度尽可能的 ...

  7. 图解图库Janusgraph系列-一文知晓图数据底层存储结构

    Python实战社群 Java实战社群 长按识别下方二维码,按需求添加 扫码关注添加客服 进Python社群▲ 扫码关注添加客服 进Java社群▲ 作者丨匠心Java 来源丨匠心Java(code-t ...

  8. Redis面试题:基本数据类型与底层存储结构

    最近在整理有关redis的相关知识,对于redis的基本数据类型以及其底层的存储结构简要的进行汇总和备注(主要为面试用

  9. Linux从入门到精通系列之线性表链式存储结构-单链表原理解析

    前言 线性表的链式存储结构的特点就是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以在内存中未被占用的任意位置. 比起顺序存储结构每个元素只需要存储一个位置就可以了.现在链式存储结构中,除了 ...

最新文章

  1. 线性代数同济第六版_线性代数考试内容与课后习题
  2. T-SQL 之 执行顺序
  3. 实现简单的ImageLoader
  4. EBB-4、忘记root密码;文件,目录权限
  5. slf4j和log4j的使用
  6. printf打印二进制_优雅地打印 HEX 数据
  7. [转载]修改SDE权限造成无法在ArcMap中绘制图形的解决办法
  8. 黑马程序员——C语言基础教程笔记
  9. PPT小技巧:拆解汉字!
  10. 【深度长文】中国电子商务简史:1999-2019
  11. PS教程新手入门(一)--去除图片上文字的方法(6种)
  12. c语言中dot作用,Unix中的dot命令详解
  13. ORMLite 数据库的使用--二次封装
  14. Python入门基础教程(打印一段文字)
  15. leaftlet 中Polygon的使用属性
  16. 【LeetCode】377. 组合总和 Ⅳ(错题2刷)
  17. 网摘:一位网友《塑造阳光心态》的学习心得
  18. Windows10下VTR.7中VPR项目的运行
  19. Linux | 项目自动化构建工具 - make/Makefile
  20. 编译安装常用包+阿里镜像源-常用资源-系统-下载-科莱软件下载-docker仓库包-安全圈-杏雨梨云-图形界面安装...

热门文章

  1. 颠覆传统翻译软件的ChatGPT翻译软件
  2. 推荐十三个帮助你提高远程工作效率的工具
  3. 如今传统企业如何做数字化转型?
  4. 北森继续冲刺港交所上市:累计亏损约54亿元,王朝晖为董事长
  5. 《Cell》文章揭示嘌呤饥饿为潜在的IBS治疗靶标?
  6. latex图片引用显示问号??
  7. Burp Suite 是用于攻击web 应用程序的集成平台
  8. GRIR重分类(上)
  9. IDC行业的发展与速度是怎么样的
  10. 地图建筑群的光影效果原理和应用实践