学习笔记之HashMap篇,简单学习了解HashMap的实现原理和扩容。

大家都知道HashMap处理数据很快,时间复杂度O(1),那么是怎么做到的呢?那就先了解一下常见数据结构。

一般来说,我们把存储结构分为两种个,顺序存储结构和链式存储结构,那我们就以最常见的两种,数组和链表为例。

数组

数组采用的一段连续的存储单元来存储数据,我们可以通过数组的下表来进行查找数据,时间复杂度为O(1),如果通过给定的值来查找需要遍历数组,所以时间复杂度为O(n),当然在有序的情况下我们可以加速这个对比过程,通过二分查找可以实现时间复杂度O(logn),插入删除元素的话需要一个一个处理元素位置,所以也是O(n)级别。

链表

链表的话存储数据不需要连续的存储单元,只需要在当前数据中存储下一个来实现链表,这样我们无法像数组那样通过下表查找,在查找数据时只能一个一个往后找,所以时间复杂度为O(n),但是链表的优势就在于插入删除操作只需要处理一下结点的引用就可以了,所以时间复杂度O(1)。

既然数组和链表各有优势,那我们能不能结合他们的优势呢?哈希表应运而生。

哈希表简单介绍

上面说到了数组可以通过下标查找数据,时间复杂度O(1),哈希表就利用这个优势,所以哈希表的主干就是一个数组,那么问题来了,我知道下标才能快速取啊,然而我现在只有值,怎么通过存储元素的值来确定他的下标呢?这里,我们就要通过哈希函数来把这个元素值映射到对应的下标,至于这个函数,我们就不详细介绍了,简单来说就是取这个元素值的哈希值来做模运算从而获得下表位置,通过这个位置来实现快速读取。说到了哈希函数,这个函数的设计尤为关键,直接影响到性能,因为这个函数设计的不好,可能导致很多数存储在了同一个下标下。那么看到这又该发现问题了,一个下标下怎么存储很多元素呢?这便是哈希冲突的问题。

哈希冲突

正如上面所讲,哈希冲突就是我们在通过哈希函数来计算下标的时候出现了重复,当一个元素要存进去的时候发现里面已经被占了,这便是哈希冲突,也被叫做哈希碰撞。我们前面也说过了,数组需要连续的存储单元来存储数据,所以再好的哈希函数的设计也不可能做到不出现哈希冲突,所以就出现了几种解决哈希冲突的方法:开放定址法,在散列函数法,还有链地址法。

我们的HashMap使用的就是链地址法,也就是主干为一个数组,而在每个位置上存放的又是一个链表,这实际上就是一个链表散列的数据结构。这也就是前面我说哈希函数设计的好坏直接影响性能的一个原因,哈希函数的设计原则是要做到计算简单和散列地址分布均匀,分布不均匀导致的结果就是一个位置上出现一个很长的链表,我们找到这个位置再去寻找数据的时候有需要遍历链表上的数据来寻找,这就导致了读取数据的性能下降。

HashMap的实现原理

下面我们一起看一点源码。

 public class HashMap<K,V>
extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable

先来看定义,HashMap类继承了AbstractMap类,实现了Map,Cloneable和Serializable接口。

     /**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30; /**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

这里看定义的几个常量,DEFAULT_INITIAL_CAPACITY是默认初始容量16,MAXIMUM_CAPACITY最大容量2的30次方,DEFAULT_LOAD_FACTOR默认加载因子0.75。

     /**
* The number of key-value mappings contained in this map.
*/
transient int size; /**
* The next size value at which to resize (capacity * load factor).
* @serial
*/
// If table == EMPTY_TABLE then this is the initial capacity at which the
// table will be created when inflated.
int threshold; /**
* The load factor for the hash table.
*
* @serial
*/
final float loadFactor; /**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;

再来看几个比较重要的变量:

size是指当前哈希表中键值对的数量,源码中还有size()方法来返回了这个变量size。

threshold在源码中的注释为The next size value at which to resize (capacity * load factor).如果我没理解错的话threshold是指下一个要进行扩容的值,通常是容量*加载因子。

loadFactor就是哈希表的加载因子。

modCount是用来快速失败的一个值,因为HashMap不是线程安全的,所以当多个线程导致了HashMap内部结构发生改变时,需要抛出异常。

     static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash; /**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
} public final K getKey() {
return key;
} public final V getValue() {
return value;
} public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
} public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
} public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
} public final String toString() {
return getKey() + "=" + getValue();
} /**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {
} /**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
}

这一部分代码很长,这是HashMap中的一个内部类Entry,前面我们也说到HashMap是数组加链表的结构,主干数组上每个位置就是Entry,Entry就是HashMap中的一个基本组成元素。

这段源码中后面的类中的方法就不详细研究了,看一下2-5行定义的几个变量,首先是key和value,每个Entry中就是一个key-value键值对。

第4行next,这个next存储就是指向下一个Entry的引用,就是通过这个next形成了一个单链表的结构,进而形成了主干数组上放链表的HashMap的结构。

第5行hash,对key的hashcode值进行hash运算后得到。

 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

这一行代码就是HashMap的主干数组。

下面看一下HashMap的存取

HashMap中的存取

     public V get(Object key) {
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue();
} private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}

首先是get方法,当key等于null的时候,调用getForNullKey方法。那我们先来看getForNullKey方法,首先判断当前HashMap的当前元素数量,如果为0返回null,否则的话先定位到主干数组下标为0的位置,然后遍历Entry链表,一个一个找key为null的那一个,如果有,返回对应的value值,如果没有,返回null。

那当key不是null的时候,调用了一个getEntry的方法,源码如下

     final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
} int hash = (key == null) ? 0 : hash(key);
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 != null && key.equals(k))))
return e;
}
return null;
}

和上面的getForNullKey方法很相似,先看size是否为0,然后用hash方法得到hash,然后通过indexFor的方法传入hash和数组长度得到这个key所存储的下标位置,然后遍历Entry数组,寻找那个要找的key,返回这个Entry,如果没有则返回null。

再回到上面代码,得到这个Entry以后再返回他的value。

这其中一些细节是没有深入研究的,先明白大体过程,慢慢深入了解细节。

下面看put方法。

     public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
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;
} /**
* Offloaded version of put for null keys
*/
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}

首先判断数组是否{}空数组,如果是的话,进行数组填充为数组分配实际存储空间,如果要存储的键值对中key为null,调用putForNullKey方法,大体操作过程就是在下标为0的地方遍历该位置上的Entry链表,如果发现已经存在null这个key,那就覆盖掉以前的value,如果没有,那就创建一个新的Entry接在链表上。

同样的,如果不为null进行的操作也基本类似,获得hash,通过hash和数组长度获取下标,定位到下标对应的Entry链表遍历对比,已经存在就覆盖,没有就创建新的接在后面。

HashMap的扩容

首先得知道什么时候扩容,就是当当前size达到阈值,也就是前面提到的threshold,容量*加载因子时,就要自动扩容了。

这里要提到的就是resize,重新计算容量,当我们不停地向HashMap中添加元素时,HashMap内部数组无法装下更多元素,就需要扩大数组的长度来装更多的元素,当然,数组无法扩容,所以我们使用的方法就是用一个更大的数组来代替小的数组。

     void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
} Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

先上resize方法的源码,先引用扩容前的数组,得到长度,然后第4行的if,是判断扩容前的数组是否已经达到最大值MAXIMUM_CAPACITY,也就是2的30次方,如果已经到了,那就修改阈值为Integer.MAX_VALUE,也就是int的最大值2的31次方减1,这样以后就不在扩容了。如果还没达到最大值,那先new一个新的Entry数组,调用transfer方法将数据放入新数组,然后将HashMap中的table属性引用这个新数组,然后得到新的阈值。

这里使用了transfer方法来拷贝,下面看一下这个方法。

     void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}

transfer方法首先引用了旧的Entry数组,遍历这个旧数组,每循环一次,用Entry对象e获取到这个元素,并且将数组中该位置的Entry对象的引用释放,然后嵌套了一个do-while循环遍历链表,当前上的元素重新计算在新数组中对应的下标,断开与后一个之间的连接,指向目标位置(这样的结果就是发生哈希冲突时元素往同一个下标上位置放的时候会插入到链表头,先放的会放到尾部),然后将该元素放在数组上,e再指向next,直到把链表中的每个元素重新分配,然后外层循环继续循环到旧数组的下一个下标处。

看起来很乱,通俗点讲就是俩循环遍历了数组上的每个链表上的每个元素,重新计算了他们在新数组的位置并且挪过去。这就是一个rehash再散列的过程。

这一篇就到这,大佬们的支持是我努力学习的动力,哪里有问题请多帮我指正。

HashMap实现原理的更多相关文章

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

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

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

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

  3. HashMap的原理与实 无锁队列的实现Java HashMap的死循环 red black tree

    http://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html https://zh.wikipedia.org/wiki/%E7%BA ...

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

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

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

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

  6. Java HashMap工作原理及实现

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

  7. HashMap实现原理和源码解析

    哈希表(hash table)也叫散列表,是一种非常重要的数据结构.许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,本文会对java集合框架中的对应实现HashMap的 ...

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

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

  9. HashMap 实现原理

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

  10. 【JAVA】HashMap的原理及多线程下死循环的原因

    再次翻到以前工作中遇到的一个问题,HashMap在多线程下会出现死循环的问题,以前只是知道会死循环,导致CPU100%把机器拖跨,今天来彻底看看 首先来看下,HashMap的原理:HashMap是一个 ...

随机推荐

  1. 线性表之何时使用ArrayList、LinkedList?

    前言 线性表不仅可以存储重复的元素,而且可以指定元素存储的位置并根据下表访问元素. List接口的两个具体实现:数组线性表类ArrayList.链表类LinkedList. ArrayList Arr ...

  2. poj 3648 2-SAT建图+topsort输出结果

    其实2-SAT类型题目的类型比较明确,基本模型差不多是对于n组对称的点,通过给出的限制条件建图连边,然后通过缩点和判断冲突来解决问题.要注意的是在topsort输出结果的时候,缩点后建图需要反向连边, ...

  3. 文件系统的几种类型:ext3, swap, RAID, LVM

    分类: 架构设计与优化 1.  ext3 在异常断电或系统崩溃(不洁关机, unclean system shutdown  ).每个已挂载ext2文件系统计算机必须使用e2fsck程序来检查其一致性 ...

  4. 团队作业8——第二次项目冲刺(Beta阶段)--第三天

    一.Daily Scrum Meeting照片 二.燃尽图 三.项目进展 学号 成员 贡献比 201421123001 廖婷婷 16% 201421123002 翁珊 16% 201421123004 ...

  5. 团队作业八-Beta版本冲刺计划及安排

    Beta版本冲刺计划及安排 目录: 1.介绍小组新加入的成员,他担任的角色 2.下一阶段需要改进完善的功能 3.下一阶段新增(或修改)的功能 4.需要改进的团队分工 5.需要改进的工具流程 6.冲刺的 ...

  6. 201521123052《Java程序设计》第2周学习总结

    1. 本周学习总结 String类一些用法 学习Array类 使用枚举 使用eclipse关联JDK源代码 使用码云管理云代码 2.书面作业 1.使用Eclipse关联jdk源代码,并查看String ...

  7. 201521123012 《Java程序设计》第十周学习总结

    1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结异常与多线程相关内容. 2. 书面作业 1.本次PTA作业题集异常.多线程 finally 题目4-2 1.1 截图你的提交结果(出 ...

  8. 【DDD】业务建模实践 —— 删除帖子

    本文是基于上一篇‘业务建模战术’的实践,主要讲解‘删除帖子’场景的业务建模,包括:业务建模.业务模型.示例代码:示例代码会使用java编写,文末附有github地址.相比于<领域驱动设计> ...

  9. apache: apache-tomcat-6.0.35完整下载

    Index of /dist/tomcat/tomcat-6/v6.0.35/bin Name Last modified Size Description Parent Directory - ex ...

  10. 从文本中读取内容并把读取到的内容转化成二进制保存的形式(包含十进制数如何转换成二进制数dtob函数)

    #include<stdio.h> #include<string.h> #include<stdlib.h> #include<malloc.h> d ...