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. 美团点评2017校招笔试真题-算法工程师B

    美团点评2017校招笔试真题-算法工程师B 1.以下关于经典的k-means聚类的说法哪个是错误的? A:k-means聚类算法是全局收敛的 B:k-means的聚类结果和初始聚类中心点的选取有关 C ...

  2. jQuery链式调用

    <script> var arr = function(){ return new arr.prototype.init(); } arr.prototype.init = functio ...

  3. ansible-playbook api 2.0 直接运行

    官方文档见  http://docs.ansible.com/ansible/dev_guide/developing_api.html 拿官方的例子修改如下 import json from col ...

  4. MongoDB操作数据库的几个命令(自己用)

    本文以配置百度的Clouda为源头:http://cloudajs.org/docs 继而安装node.js:http://nodejs.org/download/(直接安装,简单) 和MongoDB ...

  5. (KMP扩展 利用循环节来计算) Cyclic Nacklace -- hdu -- 3746

    http://acm.hdu.edu.cn/showproblem.php?pid=3746 Cyclic Nacklace Time Limit: 2000/1000 MS (Java/Others ...

  6. java基础-day4

    第04天 java基础语法 今日内容介绍 u Random u 数组 第1章   Random 1.1      产生整数随机数 1.1.1    Random的使用步骤 我们想产生1~100(包含1 ...

  7. 转:解决windows下eclipse中android项目关联android library project失败问题

    近日,在做一个人人的第三方小项目.打算直接使用renren 的sdk 进行开发.因为renren的sdk是以android library project 形式发布的(关于这种project的内容可以 ...

  8. 对话框的按键处理 PreTranslateMessage、OnKeyDown和OnChar

    对话框的按键处理 PreTranslateMessage.OnKeyDown和OnChar 1.MFC对话框不能响应OnKeyDown和OnChar函数(1)现象  在MFC的对话框中,映射了WM_C ...

  9. delphi TStringList 用法详解

    转自: http://blog.163.com/you888@188/blog/static/67239619201472365642633/ delphi TStringList 用法详解 2014 ...

  10. Html隐藏占空间与隐藏不占空间

    隐藏不占用空间: display:none; 以下为示例代码: <span style="display:none;"> 获取中</span> 隐藏占用空间 ...