HashMap是java中相当重要的数据结构,使用HashMap的场景非常之多,因此,了解HashMap实现的过程和原理,是非常有必要的,在一些面试中也会经常被问到。好了,我们赶紧来研究java内部是怎么实现HashMap的吧!

首先,我们都知道,数组的元素查找的效率是不错的,但是涉及到插入操作和删除操作,效率低下,因为可能会涉及到后续元素位置的迁移。而另外一个数据结构链表则很好的解决了这个问题,插入和删除操作都只需要改变节点的指针就行,但是链表的检索的效率就很低了,试想一下,要检索的元素在链表的末尾,而我们只能从链表头开始走完整个链表,才能检索到这个元素。而Hash表能给我们带来高效查询,插入和删除。它是怎么做到的呢?

Hash表的实质是构造记录的存储位置和其对应的关键字之间的映射函数f,关于Hash函数的构造方法,主要有如下几种:

(1)直接定址法,取关键字的某个线性函数作为Hash函数即Hash(key) = a*key+b。这种方法很少使用,虽然不会发生冲突,但是当key非常多的时候,整张Hash表也会非常大,毕竟是一一映射的。

(2)平方取中法,将key的平方的中间几位数作为得到的Hash地址。

(3)除留余数法,将key除以某个数,得到的余数作为Hash地址。

还有一些方法我们在此就不说了。当多个关键字经过这个Hash函数的映射而得到同一个值的时候,就发生了Hash冲突。解决Hash冲突主要有两种方法:

(1)开放定址法:

其中i=1,2,3。。。。,k(k<=m-1),H(key)为哈希函数,m为哈希表表长,di为增量序列,可能有下列2种情况:

当 di=1,2,3....,m-1时,称线性探测在散列;

当    时,称为二次探测再散列。

(2)链地址法:

即将所有关键字为同义词的记录存储在同一线性表中。假设某哈希函数产生的哈希地址在区间[0,m-1]上,则设立一个指针型向量 ChainHash[m];

其每个分量的初始状态都是空指针。凡是哈希地址为i的记录都插入到头指针为ChainHash[i]的链表中。在列表中的插入位置可以在表头或表尾;也可以在中间,以保持同义词在同一线性表中按关键字有序。

例如:已知一组关键字为(19,14,23,01,68,20,84,27,55,11,10,79),则按哈希函数H(key)=key MOD 13 和链地址法处理冲突构造所得的哈希表,如下图所示:

Java中的HashMap的基本结构就如上图所示,竖着看是一个数组,横着看就是多个链表。当新建一个HashMap的时候,就初始化了一个数组:

 /**
* The table, resized as necessary. Length MUST Always be a power of two.
*/ transient Entry[] table;

关于transient关键字,是为了使其修饰的对象不参与序列化,也就是说,这个对象无法被持久化,这里用这个关键字是有原因的,由于HashCode()方法是一个本地方法(由java调用本地的外部函数执行),所以不同的虚拟机,对于相同的hashCode 产生的 Code 值可能是不一样的,如果你使用默认的序列化,那么反序列化后,元素的位置和之前的是保持一致的,可是由于 hashCode 的值不一样了,那么定位函数 indexOf()返回的元素下标就会不同,这样不是我们所想要的结果.举个网上大神的例子:

向HashMap存一个entry, key为 字符串"STRING", 在第一个java程序里, "STRING"的hashcode()为1, 存入第1号bucket; 在第二个java程序里, "STRING"的hashcode()有可能就是2, 存入第2号bucket. 如果用默认的串行化(Entry[] table不用transient), 那么这个HashMap从第一个java程序里通过串行化导入第二个java程序后, 其内存分布是一样的,那么我取1号bucket能拿到“STRING”这个key,取2号bucket也能拿到相同的key,这是不合理的。

因此,HashMap这个entry数组是不可以串行化的。因此,HashMap自己实现了readObject和writeObject,在其中它只保存了bucket size,entry count(这两个其实不是必需的,但有助于提高效率)和所有的key/value(这个才是必须的)。

这就是数组内的链表:

 static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; //持有的一个指向下一个元素的引用,构成链表。
....
}

下面让我们来看看Hash在put新元素时所做的操作:

 public V put(K key, V value) {   // HashMap允许存放null键和null值, 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
if (key == null)
return putForNullKey(value); //null key 存放的总是数组的第一个元素中
int hash = hash(key.hashCode()); // 根据key的HashCode重新计算hash值
int i = indexFor(hash, table.length); //通过hash值算出在对应table中的索引(下标)。
for (Entry<K,V> e = table[i]; e != null; e = e.next) { // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //如果存在key相等的情形时,则用新的value值覆盖老的value的值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++; // 如果i索引处的Entry为null,表明此处还没有Entry。
addEntry(hash, key, value, i); // 将key、value添加到i索引处。
return null;
}

怎么新加传进来的entry呢:

 void addEntry(int hash, K key, V value, int bucketIndex) {   // 获取指定 bucketIndex 索引处的 Entry,bucketIndex可以理解为存放的table中的index,当这个bucketIndex相同时,就是发生了Hash冲突,
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e); // 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry
4 if (size++ >= threshold) // 如果新加入后的大小超过了当前最大容量,则把 table 对象的长度扩充到原来的2倍。
resize(2 * table.length);
}

原来新加的entry都是加在了链表的头端。

在取Entry的时候就非常简单了,如果key等于null,直接取数组的第一个元素,如果不是,先计算出key的hashcode找到下标,再用key的equals方法判断是否相等,如果相等,则返回对应的entry,如果不相等,则返回null:

 public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}

关于HashMap的存储原理就说到这里啦,有什么错误,请欢迎指正。

参考资料:http://zhangshixi.iteye.com/blog/672697

HashMap的存储原理的更多相关文章

  1. 牛客网Java刷题知识点之HashMap的实现原理、HashMap的存储结构、HashMap在JDK1.6、JDK1.7、JDK1.8之间的差异以及带来的性能影响

    不多说,直接上干货! 福利 => 每天都推送 欢迎大家,关注微信扫码并加入我的4个微信公众号:   大数据躺过的坑      Java从入门到架构师      人工智能躺过的坑          ...

  2. List、Set集合系列之剖析HashSet存储原理(HashMap底层)

    目录 List接口 1.1 List接口介绍 1.2 List接口中常用方法 List的子类 2.1 ArrayList集合 2.2 LinkedList集合 Set接口 3.1 Set接口介绍 Se ...

  3. HashMap的工作原理

    HashMap的工作原理   HashMap的工作原理是近年来常见的Java面试题.几乎每个Java程序员都知道HashMap,都知道哪里要用HashMap,知道HashTable和HashMap之间 ...

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

    ①HashMap的工作原理 HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象.当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算h ...

  5. HashMap的工作原理深入再深入

    前言 首先再次强调hashcode (==)和equals的真正含义(我记得以前有人会说,equals是判断对象内容,hashcode是判断是否相等之类): equals:是否同一个对象实例.注意,是 ...

  6. [转] HashMap的工作原理

    HashMap的工作原理是近年来常见的Java面试题.几乎每个Java程序员都知道HashMap,都知道哪里要用HashMap,知道Hashtable和HashMap之间的区别,那么为何这道面试题如此 ...

  7. 【转】HashMap的工作原理

    很好的文章,推荐Java的一个好网站:ImportNew HashMap的工作原理是近年来常见的Java面试题.几乎每个Java程序员都知道HashMap,都知道哪里要用HashMap,知道Hasht ...

  8. 转:HashMap的工作原理,及笔记

    HashMap的工作原理是近年来常见的Java面试题.几乎每个Java程序员都知道HashMap,都知道哪里要用HashMap,知道Hashtable和HashMap之间的区别,那么为何这道面试题如此 ...

  9. HashMap的工作原理(转)

    HashMap的工作原理是近年来常见的Java面试题.几乎每个Java程序员都知道HashMap,都知道哪里要用HashMap,知道Hashtable和HashMap之间的区别,那么为何这道面试题如此 ...

随机推荐

  1. C语言之运算符和条件结构

    表达式:是有操作数和运算符组成的. 操作数:常量.变量.子表达式 X=(x+2)*(y-2); 运算符: 赋值运算符:= .其作用是做赋值运算,将等号后边的值赋值给等号前边的. 复合赋值运算符: += ...

  2. 矢量量化(VQ)

    作者:桂. 时间:2017-05-31  21:14:56 链接:http://www.cnblogs.com/xingshansi/p/6925955.html 前言 VQ(Vector Quant ...

  3. .NET MVC页面生命周期及传统ASP.NET页面周期

    目前我主要使用.Net MVC框架进行网页创建,数据库是MSSQL Server.所以,我就用.NET MVC框架的web页面周期来说明页面的生命周期,但是我觉着其他MVC框架也是大同小异的. 本文主 ...

  4. 微信JS-SDK开发 入门指南

    目录 前言 1. 过程 1.1 代码 1.2 代理 1.3 下载 1.4 解压 1.5 运行 1.6 查看 2. 微信接口测试 2.1 申请测试帐号 2.1.1 测试号信息 2.1.2 接口配置信息 ...

  5. hibernate配置三步走

    1.导入有hibernate的required和jpa的jar包,数据库驱动jar包,日志jar包 2.创建实体类,必须有一个属性有唯一的值 package com.hui.hibernate; pu ...

  6. [BZOJ4907]柠檬

    4709: [Jsoi2011]柠檬 Time Limit: 10 Sec  Memory Limit: 128 MB Description Flute 很喜欢柠檬.它准备了一串用树枝串起来的贝壳, ...

  7. Vulkan Tutorial 16 Command buffers

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 诸如绘制和内存操作相关命令,在Vulkan中不是通过函数直接调用的.我们需要在命令缓 ...

  8. 关于CSS样式优先级学习心得

    1.未重复时候,只要有都有格式显示 2.重复时,看权值: 权值:标签 1 <类10< ID 100 PS:(*权值 > 继承(表格属性一般无法继承,有些浏览器也不支持表格继承父标签) ...

  9. Java线程间通信之wait/notify

    Java中的wait/notify/notifyAll可用来实现线程间通信,是Object类的方法,这三个方法都是native方法,是平台相关的,常用来实现生产者/消费者模式.我们来看下相关定义: w ...

  10. DataTable数据导出Excel 并且下载

    public string Excel(System.Data.DataTable dt) { //模板的路径 string strUploadPath = HttpContext.Current.S ...