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

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

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

数组

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

链表

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

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

哈希表简单介绍

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

哈希冲突

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

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

HashMap的实现原理

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

  1. public class HashMap<K,V>
  2. extends AbstractMap<K,V>
  3. implements Map<K,V>, Cloneable, Serializable

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

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

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

  1. /**
  2. * The number of key-value mappings contained in this map.
  3. */
  4. transient int size;
  5.  
  6. /**
  7. * The next size value at which to resize (capacity * load factor).
  8. * @serial
  9. */
  10. // If table == EMPTY_TABLE then this is the initial capacity at which the
  11. // table will be created when inflated.
  12. int threshold;
  13.  
  14. /**
  15. * The load factor for the hash table.
  16. *
  17. * @serial
  18. */
  19. final float loadFactor;
  20.  
  21. /**
  22. * The number of times this HashMap has been structurally modified
  23. * Structural modifications are those that change the number of mappings in
  24. * the HashMap or otherwise modify its internal structure (e.g.,
  25. * rehash). This field is used to make iterators on Collection-views of
  26. * the HashMap fail-fast. (See ConcurrentModificationException).
  27. */
  28. transient int modCount;

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

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

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

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

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

  1. static class Entry<K,V> implements Map.Entry<K,V> {
  2. final K key;
  3. V value;
  4. Entry<K,V> next;
  5. int hash;
  6.  
  7. /**
  8. * Creates new entry.
  9. */
  10. Entry(int h, K k, V v, Entry<K,V> n) {
  11. value = v;
  12. next = n;
  13. key = k;
  14. hash = h;
  15. }
  16.  
  17. public final K getKey() {
  18. return key;
  19. }
  20.  
  21. public final V getValue() {
  22. return value;
  23. }
  24.  
  25. public final V setValue(V newValue) {
  26. V oldValue = value;
  27. value = newValue;
  28. return oldValue;
  29. }
  30.  
  31. public final boolean equals(Object o) {
  32. if (!(o instanceof Map.Entry))
  33. return false;
  34. Map.Entry e = (Map.Entry)o;
  35. Object k1 = getKey();
  36. Object k2 = e.getKey();
  37. if (k1 == k2 || (k1 != null && k1.equals(k2))) {
  38. Object v1 = getValue();
  39. Object v2 = e.getValue();
  40. if (v1 == v2 || (v1 != null && v1.equals(v2)))
  41. return true;
  42. }
  43. return false;
  44. }
  45.  
  46. public final int hashCode() {
  47. return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
  48. }
  49.  
  50. public final String toString() {
  51. return getKey() + "=" + getValue();
  52. }
  53.  
  54. /**
  55. * This method is invoked whenever the value in an entry is
  56. * overwritten by an invocation of put(k,v) for a key k that's already
  57. * in the HashMap.
  58. */
  59. void recordAccess(HashMap<K,V> m) {
  60. }
  61.  
  62. /**
  63. * This method is invoked whenever the entry is
  64. * removed from the table.
  65. */
  66. void recordRemoval(HashMap<K,V> m) {
  67. }
  68. }

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

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

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

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

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

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

下面看一下HashMap的存取

HashMap中的存取

  1. public V get(Object key) {
  2. if (key == null)
  3. return getForNullKey();
  4. Entry<K,V> entry = getEntry(key);
  5.  
  6. return null == entry ? null : entry.getValue();
  7. }
  8.  
  9. private V getForNullKey() {
  10. if (size == 0) {
  11. return null;
  12. }
  13. for (Entry<K,V> e = table[0]; e != null; e = e.next) {
  14. if (e.key == null)
  15. return e.value;
  16. }
  17. return null;
  18. }

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

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

  1. final Entry<K,V> getEntry(Object key) {
  2. if (size == 0) {
  3. return null;
  4. }
  5.  
  6. int hash = (key == null) ? 0 : hash(key);
  7. for (Entry<K,V> e = table[indexFor(hash, table.length)];
  8. e != null;
  9. e = e.next) {
  10. Object k;
  11. if (e.hash == hash &&
  12. ((k = e.key) == key || (key != null && key.equals(k))))
  13. return e;
  14. }
  15. return null;
  16. }

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

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

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

下面看put方法。

  1. public V put(K key, V value) {
  2. if (table == EMPTY_TABLE) {
  3. inflateTable(threshold);
  4. }
  5. if (key == null)
  6. return putForNullKey(value);
  7. int hash = hash(key);
  8. int i = indexFor(hash, table.length);
  9. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  10. Object k;
  11. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  12. V oldValue = e.value;
  13. e.value = value;
  14. e.recordAccess(this);
  15. return oldValue;
  16. }
  17. }
  18.  
  19. modCount++;
  20. addEntry(hash, key, value, i);
  21. return null;
  22. }
  23.  
  24. /**
  25. * Offloaded version of put for null keys
  26. */
  27. private V putForNullKey(V value) {
  28. for (Entry<K,V> e = table[0]; e != null; e = e.next) {
  29. if (e.key == null) {
  30. V oldValue = e.value;
  31. e.value = value;
  32. e.recordAccess(this);
  33. return oldValue;
  34. }
  35. }
  36. modCount++;
  37. addEntry(0, null, value, 0);
  38. return null;
  39. }

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

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

HashMap的扩容

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

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

  1. void resize(int newCapacity) {
  2. Entry[] oldTable = table;
  3. int oldCapacity = oldTable.length;
  4. if (oldCapacity == MAXIMUM_CAPACITY) {
  5. threshold = Integer.MAX_VALUE;
  6. return;
  7. }
  8.  
  9. Entry[] newTable = new Entry[newCapacity];
  10. transfer(newTable, initHashSeedAsNeeded(newCapacity));
  11. table = newTable;
  12. threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
  13. }

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

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

  1. void transfer(Entry[] newTable, boolean rehash) {
  2. int newCapacity = newTable.length;
  3. for (Entry<K,V> e : table) {
  4. while(null != e) {
  5. Entry<K,V> next = e.next;
  6. if (rehash) {
  7. e.hash = null == e.key ? 0 : hash(e.key);
  8. }
  9. int i = indexFor(e.hash, newCapacity);
  10. e.next = newTable[i];
  11. newTable[i] = e;
  12. e = next;
  13. }
  14. }
  15. }

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. python--对配置文件进行搜索,增加新的内容

    要求: 文件haproxy1.查 输入:www.oldboy.org 获取当前backend下的所有记录2.新建 输入: arg = {'backend': 'www.oldboy.org','rec ...

  2. 201521123019 《Java程序设计》第5周学习总结

    1. 本章学习总结 2. 书面作业 一.代码阅读:Child压缩包内源代码 1.1 com.parent包中Child.java文件能否编译通过?哪句会出现错误?试改正该错误.并分析输出结果. Ans ...

  3. 201521123037 《Java程序设计》第4周学习总结

    1. 本周学习总结 1.1 尝试使用思维导图总结有关继承的知识点. 1.2 使用常规方法总结上课内容. 1.识别类.对于一个系统中,对于名词大多为类或属性,对于动词大多为方法. 2.注释.类注释.方法 ...

  4. 201521123015 《Java程序设计》第4周学习总结

    本周学习总结 1.1 尝试使用思维导图总结有关继承的知识点. 1.2 使用常规方法总结其他上课内容. 1.多态:使用单一接口操作多种类型的对象. 2.private修饰属性,public修饰方法. 3 ...

  5. [转载] sublime text 2 调试python时结果空白

    之前用的时候都一切正常,今天突然就出现了这个问题.按ctrl+b执行的时候结果只有空白,查了很多文章都只提到了中文路径.系统路径等等,没有解决问题,直到看到了这篇文章:http://384444165 ...

  6. Emacs操作指南

  7. 201521123069 《Java程序设计》 第9周学习总结

    1. 本章学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结异常相关内容. (1)使用try...catch语句捕获异常(try块后可跟一个或多个catch块,注意子类异常要放在父类异常前面, ...

  8. 201521123011《Java程序设计》第10周学习总结

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

  9. HashMap 学习心得

    1.构造 HashMap 底层数据结构线性数组,HashMap有一个静态内部类Entry,Entry有四个属性,key,value,next,hash Entry就是HashMap键值对实现的一个基础 ...

  10. MongoDB 分布式架构 复制 分片 适用性范围

    转载自 http://www.mongoing.com/archives/3573