HashMap解析(一)

平时一直再用hashmap并没有稍微深入的去了解它,自己花点时间想往里面在深入一点,发现它比arraylist难理解很多。

数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端。

数组:数组存储区间是连续的,占用内存严重,故空间复杂的很大。但数组的二分查找时间复杂度小,为O(1);数组的特点是:寻址容易,插入和删除困难;

链表:链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)链表的特点是:寻址困难,插入和删除容易

一、HashMap的数据结构

    HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。看下面图;来理解:

从上图中可以看出,HashMap底层就是一个数组结构,只数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。

transient Entry[] table;

static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
……
}

可以看出,Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。

二、HashMap的存取实现

存储

public V put(K key, V value) {
// HashMap允许存放null键和null值。
// 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。
if (key == null)
return putForNullKey(value);
// 根据key的keyCode重新计算hash值。
int hash = hash(key.hashCode());
// 搜索指定hash值在对应table中的索引。
int i = indexFor(hash, table.length);
// 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。
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;
}
}
// 如果i索引处的Entry为null,表明此处还没有Entry。
modCount++;
// 将key、value添加到i索引处。
addEntry(hash, key, value, i);
return null;
}

从上面的源代码中可以看出:当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,根据hash值得到这个元素在数组中的位置(即下标), 如果数组该位置上已经存放有其他元素了,那

么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

ddEntry(hash, key, value, i)方法根据计算出的hash值,将key-value对放在数组table的i索引处。addEntry 是 HashMap 提供的一个包访问权限的方法,代码如下:

void addEntry(int hash, K key, V value, int bucketIndex) {
// 获取指定 bucketIndex 索引处的 Entry
Entry<K,V> e = table[bucketIndex];
// 将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 如果 Map 中的 key-value 对的数量超过了极限
if (size++ >= threshold)
// 把 table 对象的长度扩充到原来的2倍。
resize(2 * table.length);
}

当系统决定存储HashMap中的key-value对时,完全没有考虑Entry中的value,仅仅只是根据key来计算并决定每个Entry的存储位置。我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的

存储位置之后,value 随之保存在那里即可。

读取

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;
}

有了上面存储时的hash算法作为基础,理解起来这段代码就很容易了。从上面的源代码中可以看出:

从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。

归纳

     1)hashMap的key允许为null,当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。

2)判断是否key值唯一的标准,是通过对key值的hashCode计算得出,通过通过key获取value值时也是计算key的hashCode的值去找value值。

3)HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定

其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。

三、hashmap源码解读

HashMap有两个参数影响其性能:初始容量加载因子。默认初始容量是16,加载因子是0.75。容量是哈希表中桶(Entry数组)的数量,初始容量只是哈希表在创建时的容量。

加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,通过调用 rehash 方法将容量翻倍。

HashMap中定义的成员变量如下:

static final int DEFAULT_INITIAL_CAPACITY = 16;// 默认初始容量为16,必须为2的幂  

static final int MAXIMUM_CAPACITY = 1 << 30;// 最大容量为2的30次方  

static final float DEFAULT_LOAD_FACTOR = 0.75f;// 默认加载因子0.75  

transient Entry<K,V>[] table;// Entry数组,哈希表,长度必须为2的幂  

transient int size;// 已存元素的个数  

int threshold;// 下次扩容的临界值,size>=threshold就会扩容  

final float loadFactor;// 加载因子  

HashMap一共重载了4个构造方法,分别为:

HashMap()
          构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap

HashMap(int initialCapacity)
          构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap

HashMap(int initialCapacity, float loadFactor)
          构造一个带指定初始容量和加载因子的空 HashMap

HashMap(Map<? extendsK,? extendsV> m)
          构造一个映射关系与指定 Map 相同的 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); // Find a power of 2 >= initialCapacity
// 这里需要注意一下
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1; // 设置加载因子
this.loadFactor = loadFactor;
// 设置下次扩容临界值
threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
// 初始化哈希表
table = new Entry[capacity];
useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
init();
}

参考

1、HashMap深度解析(一)

2、  HashMap深度解析(二)

3、  Java容器(四):HashMap(Java 7)的实现原理

4、  图解集合 4 :HashMap

水滴石穿,成功的速度一定要超过父母老去的速度! 少尉【5】

【java提高】---HashMap解析(一)的更多相关文章

  1. java.util.HashMap 解析

    HashMap 是我们经常使用的一种数据结构.工作中会经常用到,面试也会总提到这个数据结构,找工作的时候,”HashTable 和HashMap的区别“被问到过没有? 本文会从原理,JDK源码,项目使 ...

  2. java提高(9)---HashMap解析

    HashMap解析(一) 平时一直再用hashmap并没有稍微深入的去了解它,自己花点时间想往里面在深入一点,发现它比arraylist难理解很多. 数据结构中有数组和链表来实现对数据的存储,但这两者 ...

  3. JAVA提高十二:HashMap深入分析

    首先想说的是关于HashMap源码的分析园子里面应该有很多,并且都是分析得很不错的文章,但是我还是想写出自己的学习总结,以便加深自己的理解,因此就有了此文,另外因为小孩过来了,因此更新速度可能放缓了, ...

  4. java提高篇(二三)-----HashMap

    HashMap也是我们使用非常多的Collection,它是基于哈希表的 Map 接口的实现,以key-value的形式存在.在HashMap中,key-value总是会当做一个整体来处理,系统会根据 ...

  5. java提高篇---HashMap

    HashMap也是我们使用非常多的Collection,它是基于哈希表的 Map 接口的实现,以key-value的形式存在.在HashMap中,key-value总是会当做一个整体来处理,系统会根据 ...

  6. 【转】java提高篇(二三)-----HashMap

    原文网址: http://www.cnblogs.com/chenssy/p/3521565.html HashMap也是我们使用非常多的Collection,它是基于哈希表的 Map 接口的实现,以 ...

  7. Java提高(一)---- HashMap

    阅读博客 1, java提高篇(二三)-----HashMap 这一篇由chenssy发表于2014年1月,是根据JDK1.6的源码讲的. 2,Java类集框架之HashMap(JDK1.8)源码剖析 ...

  8. JAVA与DOM解析器提高(DOM/SAX/JDOM/DOM4j/XPath) 学习笔记二

    要求 必备知识 JAVA基础知识.XML基础知识. 开发环境 MyEclipse10 资料下载 源码下载   sax.dom是两种对xml文档进行解析的方法(没有具体实现,只是接口),所以只有它们是无 ...

  9. Java 面试知识点解析(一)——基础知识篇

    前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...

随机推荐

  1. Ardupilot设备驱动 IIC、SPI、USART

    设备代码层次结构 ​ Ardupilot设备驱动代码的层次结构采用 前端实现 和 后端实现 分割,前端库主要供机器代码层调用,后端库主要供前端调用.这里前端可以理解为应用层,后端理解为驱动层,前端调用 ...

  2. hive1.2.2部署

    1.解压hvie.tar,进入conf目录下,cp hive-default.xml.template hive-site.xml; 2.将hive下的新版本jline的JAR包拷贝到hadoop下: ...

  3. Leetcode题解(十)

    29.Divide Two Integers 题目 题目要求不用乘除和取模运算,实现两个整数相除: 我的第一想法就是把除法变成减法来做,这也是最初除法的定义,其实现代码如下: class Soluti ...

  4. 磁盘管理之 raid 文件系统 分区

    第1章 RAID 磁盘阵列 1.1 使用raid的目的 1)获得更大的容量 2)让数据更安全 3)读写速度更快 1.2 raid0.raid1.raid5.raid10对比 磁头 0磁道 1扇区 前4 ...

  5. AngularJS学习篇(二十三)

    AngularJS 路由 AngularJS 路由允许我们通过不同的 URL 访问不同的内容. 通过 AngularJS 可以实现多视图的单页Web应用(single page web applica ...

  6. 实现 node_modules 共享

    参考:https://segmentfault.com/a/1190000000610038 Gruntjs 作为前端工程化工具,能够很好的对前端资源进行管理(校验,合并,压缩). 久之,发现一个问题 ...

  7. java设计师初入职场,如何站稳脚跟

    本文内容一共由3部分展开 a:新人如何快速融入团队 b:如何在职场中提升自己影响力 c:如何规进行职业规划 a:如何快速融入团队   能在层层选拔下进入公司,说明你工作的能力还是得到公司的认可,不过这 ...

  8. C#中泛型之Dictionary

    1.命名空间:System.Collections.Generic(程序集:mscorlib)2.描述: 1).从一组键(Key)到一组值(Value)的映射,每一个添加项都是由一个值及其相关连的键组 ...

  9. 前端如何处理emoji表情

    这段时间在做移动端的开发, 有一个功能就是发表评论,其实这个功能本身是比较简单的, 但是在提测是的时候QA给哦提了一个bug,说输入手机自带的emoji表情发送失败了.我就奇怪了,emoji表情也是文 ...

  10. bzoj1015星球大战

    1015: [JSOI2008]星球大战starwar Description 很久以前,在一个遥远的星系,一个黑暗的帝国靠着它的超级武器统治者整个星系.某一天,凭着一个偶然的机遇,一支反抗军摧毁了帝 ...