HashMap源码阅读与解析

今天药忘吃喽~ 2022-06-16 01:36 290阅读 0赞

1、概述

  1. Hashmap是一种常用的集合类,以key-value键值对的形式存在。HashMap中,可以通过hash算法来决定key-value键值对的存储位置,从而实现key-value键值对的快速查找和存储。虽然HashMap存取效率很高,却是线程不安全的,可以通过Collections类的静态方法synchronizedMap获得线程安全的HashMap或者使用ConcurrentHashMap(以后分析),hashTable虽然线程安全,但是效率太低,已经被废弃。

2、HashMap数据结构

Center

  1. HashMap底层是一个数组,每个数组元素存放着Entry元素或者Entry元素组成的链表。Entry元素里存储着一个Key-value键值对。当有新的Entry元素要插入数组的时候,首先会根据Entry中的key值,计算hash值,然后结合数组长度,得出此Entry元素在数组中下标位置。如果存在N个数组元素计算出的数组下标一致,即出现hash冲突,那么hashMap采用链表解决这个问题。数组下标一致时,最先插入的Entry元素在链表最底层,以后再插入的Entry元素作为链表头指向下一个Entry元素。

3、HashMap关键参数和Entry类

(1)HashMap关键参数

transient Entry[] table;//存储元素的实体数组

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

int threshold; //临界值 当实际大小超过临界值时,会进行扩容threshold =加载因子*容量

final float loadFactor; //加载因子

transient int modCount;//被修改的次数

(2)Entry类

  1. //HashMap的静态内部类
  2. static class Entry<K,V> implements Map.Entry<K,V> {
  3. //key定义成final,表示在HashMap中,key值不可以替换
  4. final K key;
  5. V value;
  6. //指向下一个Entry引用
  7. Entry<K,V> next;
  8. final int hash;
  9. /**
  10. * Creates new entry.
  11. */
  12. Entry(int h, K k, V v, Entry<K,V> n) {
  13. value = v;
  14. next = n;
  15. key = k;
  16. hash = h;
  17. }
  18. public final K getKey() {
  19. return key;
  20. }
  21. public final V getValue() {
  22. return value;
  23. }
  24. public final V setValue(V newValue) {
  25. V oldValue = value;
  26. value = newValue;
  27. return oldValue;
  28. }
  29. //重写的equals方法
  30. public final boolean equals(Object o) {
  31. if (!(o instanceof Map.Entry))
  32. return false;
  33. Map.Entry e = (Map.Entry)o;
  34. Object k1 = getKey();
  35. Object k2 = e.getKey();
  36. if (k1 == k2 || (k1 != null && k1.equals(k2))) {
  37. Object v1 = getValue();
  38. Object v2 = e.getValue();
  39. if (v1 == v2 || (v1 != null && v1.equals(v2)))
  40. return true;
  41. }
  42. return false;
  43. }
  44. //重写的hashCode方法
  45. public final int hashCode() {
  46. return (key==null ? 0 : key.hashCode()) ^
  47. (value==null ? 0 : value.hashCode());
  48. }
  49. }

4、HashMap构造函数(为了分析方便,把构造函数分成两类:参数是集合类和参数不是集合类)

(1)参数不是集合类的构造函数(3种)

  1. public HashMap(int initialCapacity, float loadFactor) {
  2. //检验输入的map的初始化大小数值是否合法
  3. if (initialCapacity < 0)
  4. throw new IllegalArgumentException("Illegal initial capacity: " +
  5. initialCapacity);
  6. if (initialCapacity > MAXIMUM_CAPACITY)
  7. initialCapacity = MAXIMUM_CAPACITY;
  8. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  9. throw new IllegalArgumentException("Illegal load factor: " +
  10. loadFactor);
  11. // 确保数组容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂
  12. int capacity = 1;
  13. while (capacity < initialCapacity)
  14. capacity <<= 1;
  15. this.loadFactor = loadFactor;
  16. //当实际大小超过临界值时,会进行扩容threshold = 加载因子*容量
  17. threshold = (int)(capacity * loadFactor);
  18. //数组table的大小一定是2的n次幂
  19. table = new Entry[capacity];
  20. init();
  21. }
  22. public HashMap(int initialCapacity) {
  23. //默认的加载因子是0.75f
  24. this(initialCapacity, DEFAULT_LOAD_FACTOR);
  25. }
  26. public HashMap() {
  27. this.loadFactor = DEFAULT_LOAD_FACTOR;
  28. //默认的临界值是(16*0.75)
  29. threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
  30. //默认的数组长度是16
  31. table = new Entry[DEFAULT_INITIAL_CAPACITY];
  32. init();
  33. }

(2)参数是集合类的构造函数

  1. public HashMap(Map<? extends K, ? extends V> m) {
  2. this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
  3. DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
  4. putAllForCreate(m);
  5. }
  6. private void putAllForCreate(Map<? extends K, ? extends V> m) {
  7. for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
  8. putForCreate(e.getKey(), e.getValue());
  9. }
  10. private void putForCreate(K key, V value) {
  11. //根据key值计算hash值
  12. int hash = (key == null) ? 0 : hash(key.hashCode());
  13. //根据hash值和数组长度,计算存储的下标
  14. int i = indexFor(hash, table.length);
  15. //检查相应的数组下标元素中是否有与待插入数据key值相同的Entry元素,如果有,value值替换之
  16. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  17. Object k;
  18. if (e.hash == hash &&
  19. ((k = e.key) == key || (key != null && key.equals(k)))) {
  20. e.value = value;
  21. return;
  22. }
  23. }
  24. //在数组下标i处插入数据
  25. createEntry(hash, key, value, i);
  26. }
  27. static int indexFor(int h, int length) {
  28. return h & (length-1);
  29. }
  30. void createEntry(int hash, K key, V value, int bucketIndex) {
  31. //获取位置bucketIndex处的链表头结点引用
  32. Entry<K,V> e = table[bucketIndex];
  33. table[bucketIndex] = new Entry<>(hash, key, value, e);
  34. //map的size加1
  35. size++;
  36. }

5、put方法

  1. public V put(K key, V value) {
  2. //key值的参数校验
  3. if (key == null)
  4. return putForNullKey(value);
  5. //获取key值的hash值
  6. int hash = hash(key.hashCode());
  7. //根据key的hash值和table数组的长度得出此键值对在数组中的下标
  8. int i = indexFor(hash, table.length);
  9. //遍历整个数组,如果key值重复,那么新value值替换旧value值
  10. for (Entry<K,V> e = table[i]; e != null; e = e.next) {
  11. Object k;
  12. if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
  13. V oldValue = e.value;
  14. e.value = value;
  15. e.recordAccess(this);
  16. return oldValue;
  17. }
  18. }
  19. modCount++;
  20. addEntry(hash, key, value, i);
  21. return null;
  22. }
  23. void addEntry(int hash, K key, V value, int bucketIndex) {
  24. Entry<K,V> e = table[bucketIndex];
  25. table[bucketIndex] = new Entry<>(hash, key, value, e);
  26. //如果size加1之后,大于等于临界值之后,那么map需要进行扩容
  27. if (size++ >= threshold)
  28. resize(2 * table.length);
  29. }
  30. void resize(int newCapacity) {
  31. Entry[] oldTable = table;
  32. int oldCapacity = oldTable.length;
  33. if (oldCapacity == MAXIMUM_CAPACITY) {
  34. threshold = Integer.MAX_VALUE;
  35. return;
  36. }
  37. Entry[] newTable = new Entry[newCapacity];
  38. transfer(newTable);
  39. table = newTable;
  40. //临界值重新赋值
  41. threshold = (int)(newCapacity * loadFactor);
  42. }
  43. void transfer(Entry[] newTable) {
  44. Entry[] src = table;
  45. //获取新数组的长度(原数组的两倍长度)
  46. int newCapacity = newTable.length;
  47. //遍历原数组,拿出元素中的每个Entry值,然后计算其key值的hash和数组中的下标值,组建新数组
  48. for (int j = 0; j < src.length; j++) {
  49. Entry<K,V> e = src[j];
  50. if (e != null) {
  51. src[j] = null;
  52. do {
  53. //保存指定entry的next值
  54. Entry<K,V> next = e.next;
  55. //获取指定entry在新数组中的下标值
  56. int i = indexFor(e.hash, newCapacity);
  57. //指定entry加入新数组
  58. e.next = newTable[i];
  59. newTable[i] = e;
  60. e = next;
  61. } while (e != null);
  62. }
  63. }
  64. }

6、get方法

  1. //根据key值计算出查找数据在table数组的下标位置,然后遍历链表,获取对应key值的value值
  2. public V get(Object key) {
  3. if (key == null)
  4. return getForNullKey();
  5. int hash = hash(key.hashCode());
  6. for (Entry<K,V> e = table[indexFor(hash, table.length)];
  7. e != null;
  8. e = e.next) {
  9. Object k;
  10. if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
  11. return e.value;
  12. }
  13. return null;
  14. }

7、remove方法

  1. public V remove(Object key) {
  2. Entry<K,V> e = removeEntryForKey(key);
  3. return (e == null ? null : e.value);
  4. }
  5. final Entry<K,V> removeEntryForKey(Object key) {
  6. int hash = (key == null) ? 0 : hash(key.hashCode());
  7. int i = indexFor(hash, table.length);
  8. Entry<K,V> prev = table[i];
  9. Entry<K,V> e = prev;
  10. while (e != null) {
  11. Entry<K,V> next = e.next;
  12. Object k;
  13. if (e.hash == hash &&
  14. ((k = e.key) == key || (key != null && key.equals(k)))) {
  15. modCount++;
  16. size--;
  17. if (prev == e)
  18. table[i] = next;
  19. else
  20. prev.next = next;
  21. e.recordRemoval(this);
  22. return e;
  23. }
  24. prev = e;
  25. e = next;
  26. }
  27. return e;
  28. }

8、分析下为什么哈希表的容量一定要是2的整数次幂

  1. 首先,length2的整数次幂的话,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;其次,length2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间,因此,length2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。

  这看上去很简单,其实比较有玄机的,我们举个例子来说明:

  假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下:

  1. h & (table.length-1) hash table.length-1
  2. 8 & (15-1): 0100 & 1110 = 0100
  3. 9 & (15-1): 0101 & 1110 = 0100
  4. -----------------------------------------------------------------------------------------------------------------------
  5. 8 & (16-1): 0100 & 1111 = 0100
  6. 9 & (16-1): 0101 & 1111 = 0101
  7. 从上面的例子中可以看出:当它们和15-11110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,89会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hash值会与15-11110)进行“与”,那么 最后一位永远是0,而0001001101011001101101111101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!而当数组长度为16时,即为2n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对keyhashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。

   所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。

参考博文:http://www.cnblogs.com/ITtangtang/p/3948406.html

http://www.cnblogs.com/chenssy/p/3521565.html

发表评论

表情:
评论列表 (有 0 条评论,290人围观)

还没有评论,来说两句吧...

相关阅读

    相关 HashMap

    一.HashMap概述 基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作, 并允许使用 null 值和 null 键。此类不保证映射的顺序,特别是它

    相关 HashMap阅读

    一、导入语 HashMap是我们最常见也是最长使用的数据结构之一,它的功能强大、用处广泛。而且也是面试常见的考查知识点。常见问题可能有HashMap存储结构是什么样的?H

    相关 HashMap

    来不及整理电子版,先献丑把笔记本拍几张,随后整理。 有人问,什么年代了,还手写笔记,哈哈,如果不亲自手写一遍,我是真心记不住。很多API不用知道工作原理 一样可以使用,所以

    相关 HashMap

    埋坑 这个必须写 1,hash方法 问:为何扩容是翻倍(乘以2) 2,get方法 3,put方法 问:为什么hash值冲突后,list变为红黑树的节点是8

    相关 HashMap

    源码博客相关博客写了那么多,突然想起来都没有写一篇我们日常开发中最常见的HashMap,今天简单补一下! HashMap简介: `HashMap` 是应用更加广泛的哈希

    相关 hashMap

    源码来自jdk:1.8,和其他jdk版本可能有少许差异。 一.hashMap的实现原理     hashMap底层是一个有Node组成的数组,每个Node都有一个key

    相关 HashMap

    本文的所有图片以及源码解析内容都来自于微信公众号<java知音>,原文作者:阿进的写字台。此处仅是对该公众号分享的内容进行一下消化吸收,不作传播。想要阅读原文,可以关注这个公众