JDK1.8-HashMap源码学习

迷南。 2022-11-21 09:49 196阅读 0赞

JDK1.8-HashMap源码学习

2020-10-30

HashMap概述

1、HashMap是哈希表的Map接口非同步实现。

  • HashMap提供所有可选的映射操作,并允许null值和null键。
  • HashMap不保证集合元素的顺序,特别是它不保证该顺序恒久不变。

2、HashMap设计用来快速访问键值对,它里面的元素是没有顺序的。

3、
HashMap的数据结构:
HashMap内部是一个“链表散列”的数据结构,即数组+链表+红黑树的结合体。

HashMap底层就是一个数组结构,数组当中的每一项又是一个链表/红黑树。
新建一个HashMap的时候,就会初始化一个数组。

一、数组节点的数据结构定义

  1. /**
  2. * Basic hash bin node, used for most entries. (See below for
  3. * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
  4. */
  5. static class Node<K,V> implements Map.Entry<K,V> {
  6. final int hash;
  7. final K key;
  8. V value;
  9. Node<K,V> next;
  10. Node(int hash, K key, V value, Node<K,V> next) {
  11. this.hash = hash;
  12. this.key = key;
  13. this.value = value;
  14. this.next = next;
  15. }
  16. public final K getKey() { return key; }
  17. public final V getValue() { return value; }
  18. public final String toString() { return key + "=" + value; }
  19. public final int hashCode() {
  20. return Objects.hashCode(key) ^ Objects.hashCode(value);
  21. }
  22. public final V setValue(V newValue) {
  23. V oldValue = value;
  24. value = newValue;
  25. return oldValue;
  26. }
  27. public final boolean equals(Object o) {
  28. if (o == this)
  29. return true;
  30. if (o instanceof Map.Entry) {
  31. Map.Entry<?,?> e = (Map.Entry<?,?>)o;
  32. if (Objects.equals(key, e.getKey()) &&
  33. Objects.equals(value, e.getValue()))
  34. return true;
  35. }
  36. return false;
  37. }
  38. }

1、类的属性值有4个:

  • hash:根据关键字key计算得到的数组下标值。
  • key:即存储关键字。
  • value:与关键字关联的值。
  • next:指向下一节点的指针。这个是链表的特征,当关键字生成的hash有重复时,就使用到链表存储元素。

二、往HashMap存入元素过程

先看源码的put方法:

  1. public V put(K key, V value) {
  2. return putVal(hash(key), key, value, false, true);
  3. }

putVal()方法源码,先理解几个参数的含义:

  • hash:存储关键字KEY的哈希值。
  • value:与KEY关联的值。
  • onlyIfAbsent:如果为true,请不要更改现有值;如果为false,则更新现有值。
  • evict:如果为false,则表处于创建模式。

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

    1. boolean evict) {
    2. Node<K,V>[] tab; Node<K,V> p; int n, i;
    3. if ((tab = table) == null || (n = tab.length) == 0)
    4. // 当数组为null时,初始化数组
    5. n = (tab = resize()).length;
    6. if ((p = tab[i = (n - 1) & hash]) == null)
    7. // 根据KEY计算得到数组下标的位置没有元素,就把当前元素存放到数组下标的位置
    8. tab[i] = newNode(hash, key, value, null);
    9. else {
    10. // 产生hash冲突,即有重复元素要存放到同一个数组下标的位置
    11. Node<K,V> e; K k;
    12. if (p.hash == hash &&
    13. ((k = p.key) == key || (key != null && key.equals(k))))
    14. // 关键字相同,更新value
    15. e = p;
    16. else if (p instanceof TreeNode)
    17. // 将节点元素存储到红黑树
    18. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    19. else {
    20. // 遍历链表
    21. for (int binCount = 0; ; ++binCount) {
    22. if ((e = p.next) == null) {
    23. // 将元素存储在链表尾部
    24. p.next = newNode(hash, key, value, null);
    25. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    26. // 当链表元素个数超过8时,将链表转为红黑树,这样可以提高查询效率
    27. treeifyBin(tab, hash);
    28. break;
    29. }
    30. if (e.hash == hash &&
    31. ((k = e.key) == key || (key != null && key.equals(k))))
    32. // 遍历同一个KEY,跳出循环
    33. break;
    34. p = e;
    35. }
    36. }
    37. if (e != null) { // existing mapping for key
    38. V oldValue = e.value;
    39. if (!onlyIfAbsent || oldValue == null)
    40. e.value = value;
    41. afterNodeAccess(e);
    42. return oldValue;
    43. }
    44. }
    45. ++modCount;
    46. if (++size > threshold)
    47. // 数组元素超过负载因子,需要扩容。扩大后容量是原来的一倍
    48. resize();
    49. afterNodeInsertion(evict);
    50. return null;
    51. }

HashMap存储过程小结

1、当我们往HashMap当中存放元素的时候,首先根据key的hashCode计算hash值,根据hash值得到这个元素在数组当中的下标。

2、如果数组该位置已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的元素放在链表头,最先加入的元素放在链表尾。

3、如果该位置上没有元素,就直接将该元素放到此数组的该位置上。

三、从HashMap读取元素的过程

get()方法源码:

  1. public V get(Object key) {
  2. Node<K,V> e;
  3. return (e = getNode(hash(key), key)) == null ? null : e.value;
  4. }

getNode()方法源码:

  1. final Node<K,V> getNode(int hash, Object key) {
  2. Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  3. // 首先根据hash值计算对应下标位置,判断KEY是否相同
  4. if ((tab = table) != null && (n = tab.length) > 0 &&
  5. (first = tab[(n - 1) & hash]) != null) {
  6. // always check first node
  7. if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
  8. // KEY相同,找到对应value,返回值。
  9. return first;
  10. if ((e = first.next) != null) {
  11. if (first instanceof TreeNode)
  12. // 遍历红黑树
  13. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  14. do {
  15. // 遍历链表
  16. if (e.hash == hash &&
  17. ((k = e.key) == key || (key != null && key.equals(k))))
  18. return e;
  19. } while ((e = e.next) != null);
  20. }
  21. }
  22. return null;
  23. }

HashMap读取过程小结:

1、首先计算key的哈希值,找到数组中对应位置的某一元素。

2、如果该位置存在元素/链表组/红黑树,则通过key的equals()方法在对应位置的链表/红黑树当中找到需要的元素值。

3、如果该位置不存在元素,则返回null。

HashMap的resize操作,扩容操作

1、HashMap要扩容的原因:

当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为HashMap中数组的长度在初始化后是固定的,所以为了提高查询的效率,就要对HashMap的数组进行扩容。扩容这是在集合接口当中很常用的操作。

2、扩容的坏处:

在HashMap数组扩容后,最消耗性能的点就出现了:

原数组当中的数据必须重新计算在新数组当中的位置,并且存放进去,这就是扩容(resize)。

3、什么时候扩容?

当HashMap中的元素个数超过数组大小*loadFactor时,HashMap就会进行数组扩容操作,loadFactor的值默认为0.75,这是一个折中的取值。

默认情况下,HashMap内部的数组大小为16,那么当HashMap中元素个数超过160.75=12的时候,HashMap就会把数组的大小扩展为216=32,即数组容量增大一倍,然后重新计算每个元素在数组当中的位置,而这是一个非常消耗性能的操作。

所以如果我们已经预知HashMap当中元素的个数,那么在初始化HashMap时设置元素个数能够有效提升HashMap的性能。

4、数组扩容增大多少?

数组容量增大为原来的一倍。

5、为什么HashMap数组的长度是2的倍数?

找索引时 key 的 hash 值与数组的长度值减 1 进行与运算,长度为 2 的倍数时能减少碰撞。

发表评论

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

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

相关阅读

    相关 JDK1.8HashMap学习

    篇文章我们来学习一下HashMap的源码,这也是面试经常能问到的知识点,而之所以为什么面试会问到它,就是因为他的设计思想,是非常值得我们学习的,也是非常经典的。同时不同的...

    相关 JDK8HashMap

    > 进入这篇文章之前,我想清楚的说一说怎么去理解`HashMap`源码。它先是使用的`hash`算法,那么哈希算法需要注意的那就是怎么`hash`,怎么减少冲突,怎么避免冲突。

    相关 HashMap JDK8 分析

    概述 HashMap是基于哈希表(散列表),实现Map接口的双列集合,数据结构是“链表散列”,也就是数组+链表 ,key唯一的value可以重复,允许存储null 键nu