前言

HashMap是平时使用最多,最常用的接口。它的源码更值得学习和理解。Java 7和Java 8的HashMap源码发生了变动,这里引申介绍下2种。

继承关系

HashMap主要继承Map接口,实现了抽象类AbstractMap

image.png

Java 7 HashMap

源码相对简单,不支持并发操作,采取的是数组+链表结构,即本身是个数组,每个数组对象放置的是单向链表

image.png

注:图片从https://www.javastack.cn/article/2018/hashmap-concurrenthashmap-details/#lg=1&slide=0 获取。

说明:插入时根据对象key进行hash,找到数组对应的位置。依次进行equals判断链表是否。不相等的话,再链表后面追加操作。

Java 8 HashMap

java8的源码相对精简很多,与java 7的HashMap结构大体一样,区别就是,当单链表的长度>8时,转化为红黑树。整体结构由 数组+链表+红黑树 构成。

image.png

注:图片从https://www.javastack.cn/article/2018/hashmap-concurrenthashmap-details/#lg=1&slide=2 获取。

Node节点

HashMap每个元素都是Node节点。包含了节点的hash,key,value,next属性。如果是红黑树,那么节点是TreeNode

  1. static class Node<K,V> implements Map.Entry<K,V> {
  2. final int hash;
  3. final K key;
  4. V value;
  5. Node<K,V> next;
  6. }

初始化

这里进行初始化操作,可以指定初始化的容量大小,负载因子,决定增长的大小。

  1. public HashMap(int initialCapacity, float loadFactor) {
  2. if (initialCapacity < 0)
  3. throw new IllegalArgumentException("Illegal initial capacity: " +
  4. initialCapacity);
  5. if (initialCapacity > MAXIMUM_CAPACITY)
  6. initialCapacity = MAXIMUM_CAPACITY;
  7. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  8. throw new IllegalArgumentException("Illegal load factor: " +
  9. loadFactor);
  10. this.loadFactor = loadFactor;
  11. this.threshold = tableSizeFor(initialCapacity);
  12. }

tableSizeFor表大小

代码作用:计算出大于或等于cap的第一个2的n次幂。

  1. static final int tableSizeFor(int cap) {
  2. int n = cap - 1;
  3. n |= n >>> 1;
  4. n |= n >>> 2;
  5. n |= n >>> 4;
  6. n |= n >>> 8;
  7. n |= n >>> 16;
  8. return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  9. }

解释:

  1. 首先对cap-1操作。这里计算的是2的幂数,如果本身cap就是2的幂数,那么结果就是2*2^n次方了。
  2. 采取>>>操作获取幂数,右移补位1操作。

hash方法

拿到key,进行hash得到hash后桶的位置。

  1. static final int hash(Object key) {
  2. int h;
  3. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  4. }

解释:
(h = key.hashCode()) ^ (h >>> 16) 就是计算key.hashCode()并扩展哈希的更高位
对象hash之后可能数值特别大,这样在数组定位时,容易造成hashCode只有低位影响了定位tab操作,这里需要一位打乱下步骤。

put方法

put方法,设置key对应的value。方式:依次找到位置进行判断put操作

  1. public V put(K key, V value) {
  2. return putVal(hash(key), key, value, false, true);
  3. }
  4. /**
  5. * onlyIfAbsent 如果是true,只有在不存在key的情况下进行put操作
  6. * evict 如果是false,则这个表是创建模式。(LinkHashMap可以进一步处理)
  7. */
  8. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  9. boolean evict) {
  10. Node<K,V>[] tab; Node<K,V> p; int n, i;
  11. //如果table是null,触发resize()操作进行扩容。
  12. //第一次resize()时,容量默认是DEFAULT_INITIAL_CAPACITY(长度16)
  13. //默认阈值:DEFAULT_LOAD_FACTOR (0.75) * DEFAULT_INITIAL_CAPACITY
  14. if ((tab = table) == null || (n = tab.length) == 0)
  15. n = (tab = resize()).length;
  16. //定位数组下标,找到元素p是否为null,是的话,进行初始化操作。
  17. if ((p = tab[i = (n - 1) & hash]) == null)
  18. tab[i] = newNode(hash, key, value, null);
  19. else {
  20. Node<K,V> e; K k;
  21. //判断数组元素p是否和插入的数据相同。如果相同找到这个节点e
  22. if (p.hash == hash &&
  23. ((k = p.key) == key || (key != null && key.equals(k))))
  24. e = p;
  25. else if (p instanceof TreeNode)
  26. //如果节点是红黑树节点,进行红黑树的插入方法
  27. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  28. else {
  29. //找到了数组元素p所在的链表,依次进行链表判断操作
  30. for (int binCount = 0; ; ++binCount) {
  31. if ((e = p.next) == null) {
  32. //下一个节点为null,进行插入操作
  33. p.next = newNode(hash, key, value, null);
  34. //TREEIFY_THRESHOLD = 8
  35. //如果链表长度大于8,触发treeifyBin方法,转化为红黑树
  36. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  37. treeifyBin(tab, hash);
  38. break;
  39. }
  40. //一直找到链表对应的数值
  41. if (e.hash == hash &&
  42. ((k = e.key) == key || (key != null && key.equals(k))))
  43. break;
  44. p = e;
  45. }
  46. }
  47. //存在节点进行替换操作
  48. if (e != null) { // existing mapping for key
  49. V oldValue = e.value;
  50. if (!onlyIfAbsent || oldValue == null)
  51. e.value = value;
  52. afterNodeAccess(e);
  53. return oldValue;
  54. }
  55. }
  56. ++modCount;
  57. //进行resize()的扩容操作
  58. if (++size > threshold)
  59. resize();
  60. //插入节点之后,进行额外的操作
  61. afterNodeInsertion(evict);
  62. return null;
  63. }

解释:
这里定位tab采取的方式是 p = tab[i = (n - 1) & hash] 本身我们应该求余操作,这里 hash%n 可以转化为 (n-1)%hash
n-1的得到的二进制全部是1

resize数组扩容

对数组进行扩容操作,每次扩容后,容量是原来的2倍,并对数据进行迁移。

  1. final Node<K,V>[] resize() {
  2. Node<K,V>[] oldTab = table;
  3. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  4. int oldThr = threshold;
  5. int newCap, newThr = 0;
  6. //原来是null,进行扩容
  7. if (oldCap > 0) {
  8. if (oldCap >= MAXIMUM_CAPACITY) {
  9. threshold = Integer.MAX_VALUE;
  10. return oldTab;
  11. }
  12. //设置新的容量newCap,扩大一倍
  13. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  14. oldCap >= DEFAULT_INITIAL_CAPACITY)
  15. //对应的阈值也扩大一倍
  16. newThr = oldThr << 1; // double threshold
  17. }
  18. //原数组有值,对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候
  19. else if (oldThr > 0) // initial capacity was placed in threshold
  20. newCap = oldThr;
  21. else { // zero initial threshold signifies using defaults
  22. //对应使用 new HashMap() 初始化后,第一次 put 的时候
  23. newCap = DEFAULT_INITIAL_CAPACITY;
  24. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  25. }
  26. if (newThr == 0) {
  27. float ft = (float)newCap * loadFactor;
  28. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  29. (int)ft : Integer.MAX_VALUE);
  30. }
  31. threshold = newThr;
  32. @SuppressWarnings({"rawtypes","unchecked"})
  33. //创建新的数组进行初始化操作
  34. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  35. //如果是初始化数组的,这里就结束了,返回 newTab
  36. table = newTab;
  37. //存在oldTab进行扩容操作
  38. if (oldTab != null) {
  39. for (int j = 0; j < oldCap; ++j) {
  40. Node<K,V> e;
  41. if ((e = oldTab[j]) != null) {
  42. oldTab[j] = null;
  43. //链表e只有1个元素,直接迁移
  44. if (e.next == null)
  45. newTab[e.hash & (newCap - 1)] = e;
  46. else if (e instanceof TreeNode)
  47. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  48. else { // preserve order
  49. //对存在的链表进行保存到newTable操作
  50. //扩容时,需要将原链表也进行拆分,放到新的链表位置上去
  51. Node<K,V> loHead = null, loTail = null;
  52. Node<K,V> hiHead = null, hiTail = null;
  53. Node<K,V> next;
  54. do {
  55. next = e.next;
  56. //高低位数组判断
  57. if ((e.hash & oldCap) == 0) {
  58. if (loTail == null)
  59. loHead = e;
  60. else
  61. loTail.next = e;
  62. loTail = e;
  63. }
  64. else {
  65. if (hiTail == null)
  66. hiHead = e;
  67. else
  68. hiTail.next = e;
  69. hiTail = e;
  70. }
  71. } while ((e = next) != null);
  72. if (loTail != null) {
  73. loTail.next = null;
  74. //第一条链表放入newTab的原位置
  75. newTab[j] = loHead;
  76. }
  77. if (hiTail != null) {
  78. hiTail.next = null;
  79. //第二条链表扩容后,放入newTab的j+oldCap位置
  80. newTab[j + oldCap] = hiHead;
  81. }
  82. }
  83. }
  84. }
  85. }
  86. return newTab;
  87. }

解释:
链表扩容时,采取的是 (e.hash & oldCap) == 0 方式,是因为每次key.hashCode对容量进行取余的时候,影响的都是cap的后几位。当cap扩容之后,就会再次放大1位扩容。 详细见参考。

假设原容量n=10000,n - 1 = 1111 假设key.hash = 10001 那么ta所在的位置是 1 然后扩容一下 现在n=100000,n - 1 = 11111

那么ta所在的位置是10001

get方法

get是直接获取节点进行返回。

  1. public V get(Object key) {
  2. Node<K,V> e;
  3. return (e = getNode(hash(key), key)) == null ? null : e.value;
  4. }
  5. final Node<K,V> getNode(int hash, Object key) {
  6. Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
  7. //定位tab的第一个节点first
  8. if ((tab = table) != null && (n = tab.length) > 0 &&
  9. (first = tab[(n - 1) & hash]) != null) {
  10. //判断第一个阶段是否相同
  11. if (first.hash == hash && // always check first node
  12. ((k = first.key) == key || (key != null && key.equals(k))))
  13. return first;
  14. if ((e = first.next) != null) {
  15. //进行链表判断
  16. if (first instanceof TreeNode)
  17. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  18. do {
  19. if (e.hash == hash &&
  20. ((k = e.key) == key || (key != null && key.equals(k))))
  21. return e;
  22. } while ((e = e.next) != null);
  23. }
  24. }
  25. return null;
  26. }

remove方法

移除方法

  1. final Node<K,V> removeNode(int hash, Object key, Object value,
  2. boolean matchValue, boolean movable) {
  3. Node<K,V>[] tab; Node<K,V> p; int n, index;
  4. if ((tab = table) != null && (n = tab.length) > 0 &&
  5. (p = tab[index = (n - 1) & hash]) != null) {
  6. //定位到数组的p节点
  7. Node<K,V> node = null, e; K k; V v;
  8. //判断节点p第一个节点是不是否和
  9. if (p.hash == hash &&
  10. ((k = p.key) == key || (key != null && key.equals(k))))
  11. node = p;
  12. else if ((e = p.next) != null) {
  13. //找到此节点对应的数据
  14. if (p instanceof TreeNode)
  15. node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
  16. else {
  17. do {
  18. if (e.hash == hash &&
  19. ((k = e.key) == key ||
  20. (key != null && key.equals(k)))) {
  21. node = e;
  22. break;
  23. }
  24. p = e;
  25. } while ((e = e.next) != null);
  26. }
  27. }
  28. //找到节点并判断是否相同
  29. if (node != null && (!matchValue || (v = node.value) == value ||
  30. (value != null && value.equals(v)))) {
  31. if (node instanceof TreeNode)
  32. ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
  33. else if (node == p)
  34. tab[index] = node.next;
  35. else
  36. p.next = node.next;
  37. ++modCount;
  38. --size;
  39. afterNodeRemoval(node);
  40. return node;
  41. }
  42. }
  43. return null;
  44. }

序列化

HashMap实现了Serializable接口,所以可以进行序列化操作。但是java没有使用默认的序列化方式,而是自己重写了 writeObject/readObject 进行独自的序列化操作。

注:实现了Serializable接口时,默认会采取ObjectInputStream或者ObjectOutputStream进行序列化操作。如果对象自己重写了writeObject/readObject方法,那么将会采取对象的提供的方法。

HashMap存储的table进行了transient,所以不能进行序列化操作。

  1. transient Node<K,V>[] table;

这里为什么采取自己的序列化方法呢?是因为HashMap对象存放的位置hash是按照key的hashCode计算出来的。而不同的JVM对于hashCode的计算方式是不一样的,采取java默认的方式,那么反序列化就会错误。HashMap就默认将table,size,modCount进行transient修饰了。

writeObject:序列化时,将key,value取出来,一个个设置进去。

  1. //此处私有方法是可以实现私有的readObject和writeObject方法,而不用关心HashMap自己的那一部分。
  2. private void writeObject(java.io.ObjectOutputStream s)
  3. throws IOException {
  4. int buckets = capacity();
  5. // Write out the threshold, loadfactor, and any hidden stuff
  6. s.defaultWriteObject();
  7. s.writeInt(buckets);
  8. s.writeInt(size);
  9. internalWriteEntries(s);
  10. }
  11. // Called only from writeObject, to ensure compatible ordering.
  12. void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
  13. Node<K,V>[] tab;
  14. if (size > 0 && (tab = table) != null) {
  15. for (int i = 0; i < tab.length; ++i) {
  16. for (Node<K,V> e = tab[i]; e != null; e = e.next) {
  17. s.writeObject(e.key);
  18. s.writeObject(e.value);
  19. }
  20. }
  21. }
  22. }

readObject:

  1. private void readObject(java.io.ObjectInputStream s)
  2. throws IOException, ClassNotFoundException {
  3. // Read in the threshold (ignored), loadfactor, and any hidden stuff
  4. s.defaultReadObject();
  5. // table size等等进行默认初始化操作
  6. reinitialize();
  7. if (loadFactor <= 0 || Float.isNaN(loadFactor))
  8. throw new InvalidObjectException("Illegal load factor: " +
  9. loadFactor);
  10. s.readInt(); // Read and ignore number of buckets
  11. int mappings = s.readInt(); // Read number of mappings (size)
  12. if (mappings < 0)
  13. throw new InvalidObjectException("Illegal mappings count: " +
  14. mappings);
  15. else if (mappings > 0) { // (if zero, use defaults)
  16. // Size the table using given load factor only if within
  17. // range of 0.25...4.0
  18. float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
  19. float fc = (float)mappings / lf + 1.0f;
  20. int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
  21. DEFAULT_INITIAL_CAPACITY :
  22. (fc >= MAXIMUM_CAPACITY) ?
  23. MAXIMUM_CAPACITY :
  24. tableSizeFor((int)fc));
  25. float ft = (float)cap * lf;
  26. threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
  27. (int)ft : Integer.MAX_VALUE);
  28. // Check Map.Entry[].class since it's the nearest public type to
  29. // what we're actually creating.
  30. SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
  31. @SuppressWarnings({"rawtypes","unchecked"})
  32. Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
  33. table = tab;
  34. // Read the keys and values, and put the mappings in the HashMap
  35. for (int i = 0; i < mappings; i++) {
  36. @SuppressWarnings("unchecked")
  37. K key = (K) s.readObject();
  38. @SuppressWarnings("unchecked")
  39. V value = (V) s.readObject();
  40. putVal(hash(key), key, value, false, false);
  41. }
  42. }
  43. }

线程不安全

HashMap会进行自动扩容操作,其中会有链表的处理。如果多线程操作,会导致找不到key,或者同时修改链表,造成死循环。

键不变性

HashMap采取的key尽量保证不变,因为它的数组定位是采取hashCode的。如果对象采取为key,当对象发生变化时,HashMap中的数据不存在了,换了新的定位。

参考