权衡时空


HashMap是以键值对的方式存储数据的。

如果没有内存限制,那我直接用哈希Map的键作为数组的索引,取的时候直接按索引get就行了,可是地价那么贵,哪里有无限制的地盘呢。

如果没有时间限制的话,我可以把数据放到一个无序数组中,按顺序查找,迟早也能找到。可是time is money,光阴那么短暂,谁又等得起呢。

所以,HashMap做了个折中的策略,使用适当的时间和空间做出了权衡,具体可以归结为“链表散列法”,这是一个hash表处理冲突的经典方法。

  

  链表散列


那么什么是”链表散列法”呢?看下图:

纵向的是一个数组,数组的每一项都是一个链表。你可以把这个数组看成是N个桶,每一个桶放着一个链子。

数组是干嘛的?数组的每一项负责放链表的。

链表是干嘛的?负责放Map数据的,比如一个HashMap有两个键,一个是key1,一个是key2。那么该链表就会分出两个节点分别存放这两个键值对(每一个键值对是打包放在Entry对象中的)。

链表是怎么链起来的?Entry包含有key、value、下一个节点next、hash值等,这个next就把各个节点串了起来。

HashMap保存数据的过程为:先计算当前要保存的键值对的哈希值(决定着当前键值对要放到哪个桶中),根据这个哈希值找到对应的桶。如果桶中没有数据,那就直接放进去。如果桶中已经放了数据(也即:桶中的链条上放着一个或者多个键值对),那就顺着桶中的这个链条一个一个比对,看有没有key与当前要保存的数据的key相同。如果有相同,直接覆盖原来key的value。如果没有相同的,那么将该元素保存在链头(最早保存的元素就会跑到链尾)。

  

  装填因子


桶的数量决定了能放多少个HashMap,而具体用了多少个桶,则直接关系着查找的效率。打个比方,你去隔壁班找小明,班里有10个人,你很快就会找到小明,班里坐着100个人,你可能找半天才能找到。所以你去看HashMap的构造函数是这样的:

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;
threshold = initialCapacity;
init();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

三个构造函数都牵动着两个东西:initialCapacity,loadFactor。前者表示的是桶的初始数量(即数组大小),后者表示“装填因子”,装填因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。比如,数组初始大小为100,如果装填因子=0.6,表示当数组中存放了60个Map之后,就要把数组扩容后才能继续存放。这就是为了解决上面讲到的效率问题。

装填因子定的小了,查找数据就快些,但是浪费空间。装填因子大了,空间利用率就高,但是浪费时间。生活就是这样,顾此失彼在所难免,万事哪有两全的呢。系统权衡利弊后,默认给的装填因子是0.75,这个一般我们是不需要改动的。

  

  除留余数


那么还有个问题。拿到一个Map的哈希值,怎么决定放到哪个桶里呢?如果最后数组中的Map数据都挤到一块儿那可不行,查询就会慢。太松了也不行,浪费空间。Java用了一招“除留余数法”,保证数据在数组中分布均匀。

“除留余数法”,就是取模。比如数组的长度是100,Map的哈希值是80,用80%100,余数是80,就放到80那个位置。但是Java可不是那样干算的呦,且看源码:

void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
} createEntry(hash, key, value, bucketIndex);
}

上面代码就是HashMap中的添加Entry数据的方法。BucketIndex就是当前Map在数组中的索引。第三行扩容且不谈,重点在indexFor方法,这个方法就是”取模”。我们点进去看一下:

static int indexFor(int h, int length) {

// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";

        return h & (length-1);

}

H是Map的哈希值,length是数组的长度。它直接使用了一个h & (length - 1)。这一句其实就相当于对数组取模,但是直接用二进制的位操作,比数学计算要快的多。这也给了我们程序员一个启发,能用位运算时尽量用,提高逼格又提高效率。

  

  均匀分布


还有个有趣的地方,上面代码的注释部分:length must be a non-zero power of 2,这句是说,数组的长度必须是2的n次方。

为啥是2的n次方呢?

如果不是2的n次方,比如length为15,h分别为2,3,4。那么用h & (length -1)有:

h

Length-1

h & (length -1)

0010

1110

0010,即2

0011

1110

0010,即2

0100

1110

0100,即4

你看,随便测了三个数字,就发生了碰撞。为什么会这样呢?

这是因为:如果不是2的n次方,那么2^n – 1的最低位必然为0,而0、1分别和0作“与”运算,结果都为0。也就是说,不论h为多少,h & (length - 1)的结果最低位都是0。那么数组中最低位为1的那些位置就全部空缺着,这就导致数据在数组中分布不均匀了,继而影响了查询的效率。

读取数据的时候就简单多了,通过key的hash值找到在table数组中的索引处的Entry,然后返回该key对应的value即可。

参考资料:

http://www.cnblogs.com/chenssy/p/3521565.html

http://blog.csdn.net/zhuanshenweiliu/article/details/39177447

http://blog.csdn.net/tanggao1314/article/details/51457585#t1

http://www.importnew.com/18851.html

浅谈HashMap的内部实现的更多相关文章

  1. 浅谈SQL Server内部运行机制

    对于已经很熟悉T-SQL的读者,或者对于较专业的DBA来说,逻辑的增删改查,或者较复杂的SQL语句,都是非常简单的,不存在任何挑战,不值得一提,那么,SQL的哪些方面是他们的挑战 或者软肋呢? 那就是 ...

  2. 浅谈HashMap原理,记录entrySet中的一些疑问

    HashMap的底层的一些变量: transient Node<K,V>[] table; //存储数据的Node数组 transient Set<java.util.Map.Ent ...

  3. 浅谈HashMap与线程安全 (JDK1.8)

    HashMap是Java程序员使用频率最高的用于映射(键值对)处理的数据类型.HashMap 继承自 AbstractMap 是基于哈希表的 Map 接口的实现,以 Key-Value 的形式存在,即 ...

  4. Java重点之小白解析--浅谈HashMap与HashTable

    这是一个面试经常遇到的知识点,无论什么公司这个知识点几乎是考小白必备,为什么呢?因为这玩意儿太特么常见了,常见到你写一百行代码,都能用到好几次,不问这个问哪个.so!本小白网罗天下HashMap与Ha ...

  5. 【JDK源码分析】浅谈HashMap的原理

    这篇文章给出了这样的一道面试题: 在 HashMap 中存放的一系列键值对,其中键为某个我们自定义的类型.放入 HashMap 后,我们在外部把某一个 key 的属性进行更改,然后我们再用这个 key ...

  6. 浅谈HashMap的实现原理

    1.    HashMap概述: HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射操作,并允许使用null值和null键.此类不保证映射的顺序,特别是它不保证该顺序恒久不变 ...

  7. 浅谈 Nginx 的内部核心架构设计

    一.前言 Nginx---Ngine X,是一款免费的.自由的.开源的.高性能HTTP服务器和反向代理服务器:也是一个IMAP.POP3.SMTP代理服务器:Nginx以其高性能.稳定性.丰富的功能. ...

  8. 【Java】浅谈HashMap

    HashMap是常用的集合类,以Key-Value形式存储值.下面一起从代码层面理解它的实现. 构造方法 它有好几个构造方法,但几乎都是调此构造方法: public HashMap(int initi ...

  9. 浅谈HashMap 的底层原理

    本文整理自漫画:什么是HashMap? -小灰的文章 .已获得作者授权. HashMap 是一个用于存储Key-Value 键值对的集合,每一个键值对也叫做Entry.这些个Entry 分散存储在一个 ...

随机推荐

  1. Windows Azure Virtual Machine (34) 保护Azure虚拟机

    <Windows Azure Platform 系列文章目录> 请注意:我们在Azure上创建的虚拟机,都是可以通过公网IP地址来访问的.(直接通过虚拟机的IP地址:PIP,或者通过负载均 ...

  2. MyBatis 源码分析——动态代理

    MyBatis框架是如何去执行SQL语句?相信不只是你们,笔者也想要知道是如何进行的.相信有上一章的引导大家都知道SqlSession接口的作用.当然默认情况下还是使用DefaultSqlSessio ...

  3. Hadoop权威指南: 专有数据类型

    Writable 和 WritableComparable接口 Writable接口 ** Writable接口的主要目的是,当数据在网络上传输或从硬盘读写时,提供数据的序列化和反序列化机智 ** * ...

  4. ubuntu 16.04 的64位 安装arm-none-linux-gnueabi-gcc的步骤和问题解决

    一 首先下载arm-none-linux-gnueabi-gcc交叉编译器,根据不同的需求请在网址: https://launchpad.net/gcc-arm-embedded/+download ...

  5. 我的Java笔记

    第一章 一 计算机程序:一系列有序的指令集合. 二 java语言分为三个领域 javaSE java技术核心(桌面应用程序)qq 百度云 javaEE 企业版(面向internet的应用程序)京东 淘 ...

  6. js详解之作用域-实例

    函数如下大家可以做做看 function aa(a,b,c){ function a(){} console.log(a); console.log(aa); console.log(argument ...

  7. .Net Core 之 MSBuild 介绍

    前言 关于 .NET Core 旧版本的 sdk 介绍可以参看我以前的 这篇 文章. 8 个小时前,.NET Core 项目组释放了 .NET Core 新一轮的 sdk 工具更新,即 RC4 版本 ...

  8. substring和substr、$.extend()、$.fn.extend()、(function($){….})(jQuery)的简易讲解

    1.    JS中substring与substr的区别 Substring: 该方法可以有一个参数也可以有两个参数. l  一个参数: 示例: var str="Olive": ...

  9. 前端必备技能之Photosh切图

    切图:即从设计稿里面切出网页素材 一.使用Photoshop工具 工具的使用: 1.将文字与标尺的单位的设置为像素 2.打开这五个窗口,关闭其它窗口,保存工作区方便以后使用 3.工作区弄乱时,可以使用 ...

  10. java集合框架02——Collection架构与源码分析

    Collection是一个接口,它主要的两个分支是List和Set.如下图所示: List和Set都是接口,它们继承与Collection.List是有序的队列,可以用重复的元素:而Set是数学概念中 ...