HashMap内部数据结构
 
     HashMap内部采用数组和链表结合的方式来存取数据(见下图)。这种方式有什么好处呢? 我们知道,数组操作对于检索是O(1)的,能够很快的根据数组的下标定位对象,但是插入和删除操作不高,会引起后续节点的移动,而链表的优势是:插入和删除非常的迅速,只需要重设相应的next指针就可以了。因此HashMap是结合两者优势,这是一种折中的方案。(在java8中引入了红黑树,是对性能的更进一步优化)
 
放到HashMap的key-value对是如何保存的?
 
     HashMap类内部有一个静态内部类Entry,它继承自Map.Entry接口(不要混淆了HashMap.Entry和Map.Entry):
static class Entry <K , V> implements Map .Entry < K, V > { ... }
 
再看看HashMap内部有这么一个Entry数组:transient Entry[] table; 当我们向HashMap中put一个key-value时,HashMap会将它转换为Entry对象并存放到Entry数组中的某个位置(由hash函数计算hash值后用数组长度取模来决定存放位置的)。
 
 
HashMap内部几个重要的方法解析:
 
首先欢迎hash(int h)方法闪亮登场.... 
各位码农好,我是HashMap内部的hash方法,简单介绍一下自己:我是根据key.hashCode值进行重新计算的一个方法,因为我不是很相信他们,我相信我能做得更好:
static int hash( int h ) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ ( h >>> 12) ;
return h ^ (h >>> 7 ) ^ (h >>> 4);
}

说明:该hash算法的内部实现乍一看的确很晦涩难懂。参数值h是key.hashCode(),很明显,该方法的意图就是根据key.hasCode()的值进行重新计算。为什么要这么做呢?因为HashMap要求key的hashCode值分布越均匀性能就越好(见上图,若hash函数计算出的hash值越均匀,链表的长度也就越均衡,就不会造成有的链表长度很短,而有的链表长度特别长的情况,我们知道,链表长度越长,检索效率就越慢)。而put到HashMap作为key的对象的hashCode可能五花八门,有的对象的hashCode可能还进行过重写,这可能导致put到HashMap中的key的hashCode值分布可能会不均衡,所以HashMap就利用这样一个算法对hashCode进行重新计算,以期望得到更加分布均衡的hashCode值。

 
为什么这个算法能够提供更加均衡的hashCode值,这里有一篇文章可参考一下:http://stackoverflow.com/questions/9335169/understanding-strange-java-hash-function
 
(上述关于均衡的hashCode值原理待详细讲解)
 
再欢迎我们第二个嘉宾indexFor方法:
大家好,我也简单介绍一下自己,我是根据hash值和Entry[]的长度进行取模运算的,是不是很奇怪我为什么长成这个样子,其实我也就是伪装很好的”取模“运算而已。我觉得这样比较酷一点,而且我办事效率比其他那些取模方法要快的多。
static int indexFor( int h , int length) {
return h & (length - 1 );
}

说明:在计算机数学里有这么一个法则: 当SIZE满足大小是2的幂的时候,X % SIZE(取模操作)和 X & (SIZE-1)是等价的,不信?自己动手演算一下?因此我们知道了为什么HashMap要求我们提供的capacity大小是2的幂的原因了吧?用户提供给HashMap构造函数的capacity值可能不是2的幂,所以hashmap内部进行了验证并将其重设为最接近指定capacity值的2的幂的那个值:

int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;

介绍了前两个重要方法,下面欢迎我们熟悉的put方法出场:

public V put ( K key , V value ) {
if ( key == null)
return putForNullKey ( value) ;
int hash = hash (key . hashCode()) ;
int i = indexFor (hash , table . length) ;
for ( Entry <K , V> e = table [ i ]; e != null ; e = e. next ) {
Object k ;
//
if ( e .hash == hash && ((k = e .key ) == key || key .equals ( k))) {
V oldValue = e. value ;
e .value = value ;
e .recordAccess ( this) ;
return oldValue ;
}
}
modCount ++;
addEntry (hash , key , value , i ) ;
return null ;
}
说明:有了对hash和indexFor方法以及对前面介绍的HashMap内部数据结构的知识,理解put操作就变得非常容易了,首先判断key是不是null(HashMap是允许用null作为key),如果是就将它插入到table[0]处,否则,就先以key.hashCode()为基础调用HashMap#hash方法得到一个分布更好的hash值,然后对该值取模(indexFor)得到相应的数组位,然后,判断该数组位之前有没有存放过Entry对象,如果没有就创建一个Entry对象插入到该位置,如果该位置已经有Entry对象了,那么就依次从链表的头部开始查找有没有相同key的Entry对象,如果匹配到,就替换匹配项的值,否则,就将这个Entry元素插入到链表的最后面。理解了put操作,get操作就不再赘述了。
 
HashMap对容量的动态扩容
 
     HashMap默认容量是16(也就是初始数组(也叫桶,bucket)大小是16),内部有一个变量DEFAULT_LOAD_FACTOR(负载因子,默认0.75)就是用来指示自动扩展大小的阀值,也就是说,当我们往数组中添加的元素个数大于capacity * 0.75(3/4)的时候,HashMap会自动调用resize方法调整bucket的大小。调整大小的过程就是新建一个更大的Entry数组,然后将之前添加过的Entry对象全部复制过去,由于每个Entry对象都保存有之前计算好的hash值,当数组容量扩大时再次取模(hash % length)得到的位置就不同了,所以原本在同一个链表中的元素就可能分开了,调整容量也就是为了实现将链表长度缩短这一目的的。具体复制操作的实现见transfer方法,这也很容易理解。 
 
关于ConcurrentModificationException
 
     HashMap类是非线程安全的,当一个线程正在对HashMap对象进行迭代操作,另一个线程对HashMap对象进行了更改,或者在单个线程下,用户在迭代的循环体中不正确的更改了HashMap对象元素的大小,都经常会抛出ConcurrentModificationException异常,这就是我们说的fail-fast,迭代时,其内部处理逻辑是这样的,HashMap内部有一个用以指示修改次数的计数器(modCount),当每次进行put,remove等操作时,都会引起计数器发生变化,每次迭代开始之前会取出modCount值存放到另一个exceptedModCount局部变量中,然后每个迭代过程都会检查该exceptedModCount是否和当前modCount相等,如果不等就会抛出ConcurrentModificationException异常,因此,在有多线程并发访问的情况下,建议使用ConcurrentHashMap这个线程安全类来替代HashMap的使用(题外:使用线程安全类就能确保对它的所有操作都是线程安全的吗?答案是否定的。这属于多线程原子操作等考虑的范围,不在本章详细讨论)。
 
希望这篇文章对你有帮助!
 
参考:

理解HashMap的原理的更多相关文章

  1. 深入理解HashMap(原理,查找,扩容)

    面试的时候闻到了Hashmap的扩容机制,之前只看到了Hasmap的实现机制,补一下基础知识,讲的非常好 原文链接: http://www.iteye.com/topic/539465 Hashmap ...

  2. 理解HashMap底层原理,一个简单的HashMap例子

    package com.jl.testmap; /** * 自定义一个HashMap * @author JiangLai * */ public class MyHashMap<K,V> ...

  3. java基础之hashcode理解及hashmap实现原理及MD5

    HashCode值 1. hashcode值是int的,64位.int hashCode(). 2. java object类默认的hashcode()计算方法是根据对象的内存地址来计算的.所以可由此 ...

  4. HashMap实现原理及源码分析

    哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出 ...

  5. JVM里面hashtable和hashmap实现原理

    JVM里面hashtable和hashmap实现原理   文章分类:Java编程 转载 在hashtable和hashmap是java里面常见的容器类, 是Java.uitl包下面的类, 那么Hash ...

  6. 基础进阶(一)之HashMap实现原理分析

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

  7. Java HashMap工作原理及实现

    Java HashMap工作原理及实现 2016/03/20 | 分类: 基础技术 | 0 条评论 | 标签: HASHMAP 分享到:3 原文出处: Yikun 1. 概述 从本文你可以学习到: 什 ...

  8. HashMap实现原理及源码分析(JDK1.7)

    转载:https://www.cnblogs.com/chengxiao/p/6059914.html 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技 ...

  9. HashMap 实现原理

    深入Java集合学习系列:HashMap的实现原理   参考文献 引用文献:深入Java集合学习系列:HashMap的实现原理,大部分参考这篇博客,只对其中进行稍微修改 自己曾经写过的:Hashmap ...

随机推荐

  1. 关于EmitMapper,映射配置

    public static T Snapshoot<T>(this XtraForm form, T obj) { var config = new DefaultMapConfig(); ...

  2. javascript对数组分页

    function pagination(pageNo, pageSize, array) { var offset = (pageNo - 1) * pageSize; return (offset ...

  3. wadl 的自动生成(cxf版本3.1.1)

    官方文档 http://cxf.apache.org/docs/jaxrs-services-description.html 举例: package cn.zno; import javax.ws. ...

  4. Bluebird-NodeJs的Promise

    Promise是异步代码实现控制流的一种方式.这一方式可以让你的代码干净.可读并且健壮. 比如,你用来异步处理文件事件的回调代码: fs.readFile('directory/file-to-rea ...

  5. QGIS Server Quickstart

    http://live.osgeo.org/en/quickstart/qgis_mapserver_quickstart.html

  6. (匹配)Dolls --HDU --4160

    链接: http://acm.hdu.edu.cn/showproblem.php?pid=4160 代码: #include<stdio.h> #include<string.h& ...

  7. (博弈)Simple Game --codeforces--570B

    链接: http://codeforces.com/problemset/problem/570/B http://acm.hust.edu.cn/vjudge/contest/view.action ...

  8. ASP.NET MVC 4 中Razor 视图中JS无法调试

    解决方法 1.首先检查IE中这2个属性是否勾选了. 2.选择IE浏览器进行调试,调试方法有2种     A:采用debugger;的方法,如下图所示: 这时不用调试断点就会在debugger位置中命中 ...

  9. JavaScript中Ajax的使用

    AJAX全称为“Asynchronous javascript and XML”(异步javascript和XML),是指一种创建交互式网页应用的网页开发技术.通过在后台与服务器进行少量数据交换,AJ ...

  10. Android Studio 集成 TFS,实现安卓移动开发的持续集成和交付(DevOps)

    目录 1 集成TFS系统.... 1.1 概述.... 1.2 安装TFS插件.... 1.2.1 在线安装方式.... 1.2.2 离线安装方案.... 1.3 常见操作.... 1.3.1 新建G ...