作为一种最为常用的容器,同时也是效率比较高的容器,HashMap当之无愧。所以自己这次jdk源码学习,就从HashMap开始吧,当然水平有限,有不正确的地方,欢迎指正,促进共同学习进步,就是喜欢程序员这种开源精神。(好吧,第一篇博客有点紧张)

一. HashMap结构

HashMap在jdk1.6版本采用数组+链表的存储方式,但是到1.8版本时采用了数组+链表/红黑树的方式进行存储,有效的提高了查找时间,解决冲突。这里有一篇博客写的非常好,HashMap的结构图也画的非常清楚,鼎力推荐一下杭州Mark的HashMap源码分析。虽然是基于jdk1.6的,但是换成jdk1.8,只要链表长度大于门限时,换成红黑树就好了, 我就不具体解释HashMap的具体结构了。

二.HashMap 定义

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

继承自AbstractMap,这是一个抽象类,定义了并且实现了Map接口的基本行为,但是基本上HashMap都对其进行了重写覆盖,采用了自己更高效的方式。AbstractMap是我们自己编写Map时,可以用到的基类。(好吧,来自JAVA编程思想,还没有自己写过Map)

实现了Map接口,这里其实有一个疑问,为什么在AbstractMap中已经明显实现Map接口的情况下,还要显著在实现Map接口?这种设计方式的原因?
      其余都是标记接口。

三.重要的field属性

  1. //默认的初始化数组大小为16,为啥不直接写16(jdk1.6是这么干的)啊?confused
  2. //这里必须是2的整数幂,
  3. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
  4. //最大存储容量
  5. static final int MAXIMUM_CAPACITY = 1 << 30
  6. //默认装载因子,我理解的就是所占最大百分比,即当超过75%时,就需要扩容了
  7. static final float DEFAULT_LOAD_FACTOR = 0.75f;
  8. //这是jdk1.8新加的,这是链表的最大长度,当大于这个长度时,就会将链表转成红黑树
  9. static final int TREEIFY_THRESHOLD = 8;
  10.  
  11. //table就是存储Node 的数组,就是hash表中的桶位,后面会重点介绍Node
  12. transient Node<K,V>[] table;
  13. //实际存储的数量,则HashMap的size()方法,实际返回的就是这个值,isEmpty()也是判断该值是否为0
  14. transient int size;
  15. //HashMap每改变一次结构,不管是添加还是删除都会modCount+1,主要用来当迭代时,保持数据的一致性(不知道这么理解正确么?)
  16. //因为每一次迭代,都会检查modCount是否改变是否一致,不一致就会抛出异常。这也是为什么迭代过程中,除了运用迭代器的remove()方法外,不能自己进行改变数据
  17. //fast-fail机制
  18. transient int modCount;
  19.  
  20. //扩容的门限值,当大于这个值时,table数组要进行扩容,一般等于(cap*loadFactor)
  21. int threshold;
  22. //装载因子
  23. final float loadFactor;

四.HashMap构造器

HashMap共有四种构造器,常用的有三种,主要分为无参构造器,应用默认的参数;指定初始化容量的构造器;将原有Map装进当前HashMap的构造器。

1.无参构造器

  1. //这就是一个默认的方式,潜在的问题是初始容量16太小了,可能中间需要不断扩容的问题,会影响插入的效率,当看到后面resize()方法时,可以很明显的看到这个问题
  2. public HashMap() {
  3. this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
  4. }

2.含参包括装载因子,与容量

  1. //可以指定初始容量,以及装载因子,但是感觉一般情况下指定装载因子意义不大
  2. public HashMap(int initialCapacity, float loadFactor){
  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. this.loadFactor = loadFactor;
  12. this.threshold = tableSizeFor(initialCapacity)//这里就重新定义了扩容的门限
  13. }

可以看到此时threshold被tableSizeFor()方法重新计算了,那我们研究一下tableSizeFor方法

  1. //tableSizeFor的功能主要是用来保证容量应该大于cap,且为2的整数幂,但是这段代码没有完全看懂
  2. //我理解了一下就是将最高位依次跟后面的进行或运算,将低位全变成1,最后在n+1,从而变成2的整数幂
  3. static final int tableSizeFor(int cap) {
  4. int n = cap - 1;
  5. n |= n >>> 1;
  6. n |= n >>> 2;
  7. n |= n >>> 4;
  8. n |= n >>> 8;
  9. n |= n >>> 16;
  10. return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
  11. }

不得不说写源码的都是追求完美的大神,下面是jdk1.6的实现方式,就是简单的循环,可以看到采用位运算后效率明显提高了

  1. int capacity = 1;
  2. while (capacity < initialCapacity)
  3. capacity <<= 1;

这里可能还有一个疑问,明明给的是初始容量,为什么要计算门限,而不是容量呢?其实这也是jdk1.8的改变,它将table的初始化放入了resize()中,而且压根就没有capacity这个属性,所以这里只能重新计算threshold,而resize()后面就会根据threshold来重新计算capacity,来进行table数组的初始化,然后在重新按照装载因子计算threshold,有点绕,后面看resize()源码就清楚了。

3.仅仅指定容量

  1. //下面这种方式更好,所以当知道所要构建的数据容量大小时,最好直接指定大小,可以减除不停扩容的过程,大幅提高效率
  2. public HashMap(int initialCapacity){
  3. this(initialCapacity, DEFAULT_LOAD_FACTOR);
  4. }

4.将已有Map放入当前map中

  1. //这种方式是将已有Map中的元素放入当前HashMap中
  2. public HashMap(Map<? extends K, ? extends V> m) {
  3. this.loadFactor = DEFAULT_LOAD_FACTOR;
  4. putMapEntries(m, false);
  5. }
  1. //这里就是一个个取出m中的元素调用putVal,一个个放入table中的过程。
  2. final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
  3. int s = m.size();
  4. if (s > 0) {
  5. if (table == null) { // pre-size
  6. float ft = ((float)s / loadFactor) + 1.0F;
  7. int t = ((ft < (float)MAXIMUM_CAPACITY) ?
  8. (int)ft : MAXIMUM_CAPACITY);
  9. if (t > threshold)
  10. threshold = tableSizeFor(t);
  11. }
  12. else if (s > threshold)
  13. resize();
  14. for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
  15. K key = e.getKey();
  16. V value = e.getValue();
  17. putVal(hash(key), key, value, false, evict);
  18. }
  19. }
  20. }

五.重要的方法

1.Node 内部类

可以看到前面table就是Node数组,Node是什么?它是一个HashMap内部类(改名字了,jdk1.6就叫Entry),继承自 Map.Entry这个内部接口,它就是存储一对映射关系的最小单元,也就是说key,value实际存储在Node中。

  1. //可以看出虽然这里不叫Entry这个广泛应用的名字了,但是与jdk1.6比,还是没有什么大的变化
  2. //依然实现了Map.Entry这个内部接口,换汤没换药
  3. //其实就是最基本的链表实现方式
  4. static class Node<K,V> implements Map.Entry<K,V> {
  5. //可以发现,HashMap的每个键值对就是靠这里实现的,并没有很高大上
  6. //并且键应用了final修饰符,所以键是不可改变的!
  7. final int hash;//这里这个一般保存键的hash值
  8. final K key;
  9. V value;
  10. //next 应用于一个桶位中的链表结构,表示下一个节点键值对
  11. Node<K,V> next;
  12.  
  13. Node(int hash, K key, V value, Node<K,V> next) {
  14. this.hash = hash;
  15. this.key = key;
  16. this.value = value;
  17. this.next = next;
  18. }
  19.  
  20. public final K getKey() { return key; }
  21. public final V getValue() { return value; }
  22. public final String toString() { return key + "=" + value; }
  23. //键的hashcode与值的hashcode异或,得到hash值,并且这里应用的是最原始的hashCode计算方式,Objects
  24. public final int hashCode() {
  25. return Objects.hashCode(key) ^ Objects.hashCode(value);
  26. }
  27.  
  28. public final V setValue(V newValue) {
  29. V oldValue = value;
  30. value = newValue;
  31. return oldValue;
  32. }
  33.  
  34. public final boolean equals(Object o) {
  35. if (o == this)
  36. return true;
  37. if (o instanceof Map.Entry) {
  38. Map.Entry<?,?> e = (Map.Entry<?,?>)o;
  39. //键和值都eauals,才行,而且是Objects.equals,只能是同一对象
  40. if (Objects.equals(key, e.getKey()) &&
  41. Objects.equals(value, e.getValue()))
  42. return true;
  43. }
  44. return false;
  45. }

2.put方法

  1. //可以看到是调用的putVal的方法,并且计算了key的hash值
  2. public V put(K key, V value) {
  3. return putVal(hash(key), key, value, false, true);
  4. }
  1. // Computes key.hashCode() and spreads (XORs) higher bits of hash
  2. /** to lower. Because the table uses power-of-two masking, sets of
  3. * hashes that vary only in bits above the current mask will
  4. * always collide. (Among known examples are sets of Float keys
  5. * holding consecutive whole numbers in small tables.) So we
  6. * apply a transform that spreads the impact of higher bits
  7. * downward. There is a tradeoff between speed, utility, and
  8. * quality of bit-spreading. Because many common sets of hashes
  9. * are already reasonably distributed (so don't benefit from
  10. * spreading), and because we use trees to handle large sets of
  11. * collisions in bins, we just XOR some shifted bits in the
  12. * cheapest possible way to reduce systematic lossage, as well as
  13. * to incorporate impact of the highest bits that would otherwise
  14. * never be used in index calculations because of table bounds.
  15. */
  16. static final int hash(Object key) {
  17. int h;
  18. //这里可以看到key是有可能是null的,并且会在0桶位位置
  19. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  20. }

这里hashCode的计算也进行了改进,取得key的hashcode后,高16位与低16位异或运算重新计算hash值。

下面重点看一下put方法实现的大拿,putVal方法。

  1. /**
  2. * Implements Map.put and related methods
  3. *
  4. * @param hash hash for key
  5. * @param key the key
  6. * @param value the value to put
  7. * @param onlyIfAbsent if true, don't change existing value
  8. * @param evict if false, the table is in creation mode.
  9. * @return previous value, or null if none
  10. */
  11. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  12. boolean evict) {
  13. Node<K,V>[] tab; Node<K,V> p; int n, i;
  14. //table 没有初始化
  15. if ((tab = table) == null || (n = tab.length) == 0)
  16. //可以看到table的初始化放在了这里,是通过resize来做的,后面会分析resize()
  17. n = (tab = resize()).length;
  18.  
  19. //这里就是HASH算法了,用来定位桶位的方式,可以看到是采用容量-1与键hash值进行与运算
  20. //n-1,的原因就是n一定是一个2的整数幂,而(n - 1) & hash其实质就是n%hash,但是取余运算的效率明显不如位运算与
  21. //并且(n - 1) & hash也能保证散列均匀,不会产生只有偶数位有值的现象
  22. if ((p = tab[i = (n - 1) & hash]) == null)
  23. //当这里是空桶位时,就直接构造新的Node节点,将其放入桶位中
  24. //newNode()方法,就是对new Node(,,,)的包装
  25. //同时也可以看到Node中的hash值就是重新计算的hash(key)
  26. tab[i] = newNode(hash, key, value, null);
  27. else {
  28. Node<K,V> e; K k;
  29. //键hash值相等,键相等时,这里就是发现该键已经存在于Map中
  30. if (p.hash == hash &&
  31. ((k = p.key) == key || (key != null && key.equals(k))))
  32. e = p;
  33. //当该桶位是红黑树结构时,则应该按照红黑树方式插入
  34. else if (p instanceof TreeNode)
  35. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  1. //当该桶位为链表结构时,进行链表的插入操作,但是当链表长度大于TREEIFY_THRESHOLD - 1,就要将链表转换成红黑树
  2. else {
  3. //这里binCount记录链表的长度
  4. for (int binCount = 0; ; ++binCount) {
  5. //找到链表尾端
  6. if ((e = p.next) == null) {
  7. p.next = newNode(hash, key, value, null);
  8. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  9. treeifyBin(tab, hash);
  10. break;
  11. }
  12. if (e.hash == hash &&
  13. ((k = e.key) == key || (key != null && key.equals(k))))
  14. break;
  15. p = e;
  16. }
  17. }
  18. //替换操作,onlyIfAbsent为false
  19. //所以onlyIfAbsent,这个参数主要决定是否执行替换,当该键已经存在时,
  20. //而下面的方法则是一种不替换的put方法,因为onlyIfAbsent为true
  21. //这里其实只是为了给putIfAbsent方法提供支持,这也是jdk1.8新增的方法
  22. // public V putIfAbsent(K key, V value) {
  23. // return putVal(hash(key), key, value, true, true);
  24. // }
  25. //
  26. if (e != null) { // existing mapping for key
  27. V oldValue = e.value;
  28. if (!onlyIfAbsent || oldValue == null)
  29. e.value = value;
  30. afterNodeAccess(e);
  31. return oldValue;
  32. }
  33. }
  34. //记录改变次数
  35. ++modCount;
  36. //当达到扩容上限时,进行扩容
  37. if (++size > threshold)
  38. resize();
  39. afterNodeInsertion(evict);
  40. return null;
  41. }

下面在看一下扩容方法resize(),jdk1.8的resize相比以往又多了一份使命,table初始化部分,也会在这里完成。

  1. /**
  2. * Initializes or doubles table size. If null, allocates in
  3. * accord with initial capacity target held in field threshold.
  4. * Otherwise, because we are using power-of-two expansion, the
  5. * elements from each bin must either stay at same index, or move
  6. * with a power of two offset in the new table.
  7. */
  8.  
  9. final Node<K,V>[] resize() {
  10. Node<K,V>[] oldTab = table;
  11. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  12. int oldThr = threshold;
  13. int newCap, newThr = 0;
  14. if (oldCap > 0) {
  15. if (oldCap >= MAXIMUM_CAPACITY) {
  16. threshold = Integer.MAX_VALUE;
  17. //这时已经无法扩容了
  18. return oldTab;
  19. }
  20. //采用二倍扩容
  21. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  22. oldCap >= DEFAULT_INITIAL_CAPACITY)
  23. //可以看到新的门限也是变为二倍
  24. newThr = oldThr << 1; // double threshold
  25. }
  26. else if (oldThr > 0) // initial capacity was placed in threshold
  27. //这里就是构造器只是给了容量时的情况,将门限直接给成新容量
  28. newCap = oldThr;
  29. else { // zero initial threshold signifies using defaults
  30. //这个是采用HashMap()这个方式构造容器时,可以看到就只是采用默认值就行初始化
  31. newCap = DEFAULT_INITIAL_CAPACITY;
  32. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  33. }
  34. if (newThr == 0) {
  1. //这里可以看出门限与容量的关系,永远满足loadFactor这个装载因子
  2. float ft = (float)newCap * loadFactor;
  3. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  4. (int)ft : Integer.MAX_VALUE);
  5. }
  6. //更新门限到threshold field
  7. threshold = newThr;
  8. @SuppressWarnings({"rawtypes","unchecked"})
  9. //好吧,找它一圈终于找到table的初始化了
  10. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  11. table = newTab;
  12. //这里就是当原始table不为空时,要有一个搬家的过程,所以这里是最浪费效率的
  13. if (oldTab != null) {
  14. for (int j = 0; j < oldCap; ++j) {
  15. Node<K,V> e;
  16. if ((e = oldTab[j]) != null) {
  17. //先释放,解脱它,而不是等着JVM自己收集,因为有可能导致根本没有被收集,因为原始引用还在
  18. oldTab[j] = null;
  19. //当该桶位链表长度为1时,
  20. if (e.next == null)
  21. //重新计算桶位,然后插入
  22. newTab[e.hash & (newCap - 1)] = e;
  23. //当桶位为红黑树时
  24. else if (e instanceof TreeNode)
  25. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  26. //
  27. else { // preserve order
  28. Node<K,V> loHead = null, loTail = null;
  29. Node<K,V> hiHead = null, hiTail = null;
  30. Node<K,V> next;
  31. //这里又是一个理解不太清楚的地方了
  32. //(e.hash & oldCap) == 0,应该是表示原hash值小于oldcap,则其桶位不变,链表还 是在原位置
  33. //若>0,则表示原hash值大于该oldCap,则桶位变为j + oldCap
  34. //从结果来看等效于e.hash & (newCap - 1),只是不知道为何这样计算
  35. //而且与jdk1.6比,这里链表并没有被倒置,而jdk1.6中,每次扩容链表都会被倒置
  36. do {
  37. next = e.next;
  38. if ((e.hash & oldCap) == 0) {
  39. if (loTail == null)
  40. loHead = e;
  41. else
  42. loTail.next = e;
  43. loTail = e;
  44. }
  45. else {
  46. if (hiTail == null)
  47. hiHead = e;
  48. else
  49. hiTail.next = e;
  50. hiTail = e;
  51. }
  52. } while ((e = next) != null);
  53. if (loTail != null) {
  54. loTail.next = null;
  55. newTab[j] = loHead;
  56. }
  57. if (hiTail != null) {
  58. hiTail.next = null;
  59. newTab[j + oldCap] = hiHead;
  60. }
  61. }
  62. }
  63. }
  64. }
  65. return newTab;
  66. }

其实HashMap put方法中最重要的就是putVal和resize()这两个方法了,搞懂他们,就基本搞懂HashMap的存储方式了,因为get方法就是一个反向的过程。

putAll方法就easy了

  1. public void putAll(Map<? extends K, ? extends V> m) {
  2. putMapEntries(m, true);
  3. }

3.get方法

get方法同样也是最常用的方法(不可能光存不取啊),可以看到通过getNode方法获得节点后,直接取出Node的value属性即可。

  1. public V get(Object key) {
  2. Node<K,V> e;
  3. return (e = getNode(hash(key), key)) == null ? null : e.value;
  4. }
  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算法查找的过程,可以看到与put方法是一致的
  4. if ((tab = table) != null && (n = tab.length) > 0 &&
  5. (first = tab[(n - 1) & hash]) != null) {
  6. if (first.hash == hash && // always check first node
  7. ((k = first.key) == key || (key != null && key.equals(k))))
  8. return first;
  9. //这里就是分成两种了一个是,红黑树查找
  10. //一个是链表查找
  11. if ((e = first.next) != null) {
  12. if (first instanceof TreeNode)
  13. return ((TreeNode<K,V>)first).getTreeNode(hash, key);
  14. do {
  15. if (e.hash == hash &&
  16. ((k = e.key) == key || (key != null && key.equals(k))))
  17. return e;
  18. } while ((e = e.next) != null);
  19. }
  20. }
  21. //值得注意的是当查找不到的时候是返回null的
  22. return null;
  23. }

相比于插入由于少了扩容的部分,get查找简化了很多,这里也能看到hash算法的精髓,快速定位查找功能,不需要遍历就能查找到。

  1. //containsKey,查询是否存在某个键,getNode()大法好
  2. public boolean containsKey(Object key) {
  3. return getNode(hash(key), key) != null;
  4. }

4.remove方法

  1. //根据键,删除某一个节点,返回value值
  2. public V remove(Object key) {
  3. Node<K,V> e;
  4. return (e = removeNode(hash(key), key, null, false, true)) == null ?
  5. null : e.value;
  6. }
  1. /**
  2. * Implements Map.remove and related methods
  3. *
  4. * @param hash hash for key
  5. * @param key the key
  6. * @param value the value to match if matchValue, else ignored
  7. * @param matchValue if true only remove if value is equal
  8. * @param movable if false do not move other nodes while removing
  9. * @return the node, or null if none
  10. */
  11. final Node<K,V> removeNode(int hash, Object key, Object value,
  12. boolean matchValue, boolean movable) {
  13. Node<K,V>[] tab; Node<K,V> p; int n, index;
  14. if ((tab = table) != null && (n = tab.length) > 0 &&
  15. (p = tab[index = (n - 1) & hash]) != null) {
  16. Node<K,V> node = null, e; K k; V v;
  17. //这里的node就是 所查找到的节点
  18. if (p.hash == hash &&
  19. ((k = p.key) == key || (key != null && key.equals(k))))
  20. node = p;
  21. else if ((e = p.next) != null) {
  22. if (p instanceof TreeNode)
  23. node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
  24. else {
  25. do {
  26. if (e.hash == hash &&
  27. ((k = e.key) == key ||
  28. (key != null && key.equals(k)))) {
  29. node = e;
  30. break;
  31. }
  32. //这里p保存的是父节点,因为这里涉及到链表删除的操作
  33. p = e;
  34. } while ((e = e.next) != null);
  35. }
  36. }
  37. //这里可以看出matchValue这个参数的作用了
  38. //当matchValue为false时(即这里我们remove方法所用的),直接短路后面的运算,进行删除操作,而不用关注value值是否相等或者equals
  39. //而matchValue为true时,则只有在value值也符合时,才删除
  40. //而jdk1.8也是给了重载方法,应用于当key键与value值同时匹配时,才进行删除操作
  41. if (node != null && (!matchValue || (v = node.value) == value ||
  42. (value != null && value.equals(v)))) {
  43. if (node instanceof TreeNode)
  44. //movable这个参数竟然是应用在了树删除上,可以再看一看
  45. ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
  46. //表示该节点就是链表的头节点,则将子节点放进桶位
  47. else if (node == p)
  48. tab[index] = node.next;
  49. //删除节点后节点,父节点的next重新连接
  50. else
  51. p.next = node.next;
  52. //删除操作也是要记录进modCount的
  53. ++modCount;
  54. --size;
  55. afterNodeRemoval(node);
  56. return node;
  57. }
  58. }
  59. return null;
  60. }

这里matchValue这个参数设置很巧妙,巧妙的解决了这个方法的重用问题,当然源码里还有很多很精巧的设计。

  1. //这是jdk1.8新增的方法,可以看到只有matchValue改变成了true
  2. //只有键和值都匹配才删除
  3. public boolean remove(Object key, Object value) {
  4. return removeNode(hash(key), key, value, true, true) != null;

5.replace方法

似乎是jdk1.8新增的方法,但是有了前面的理解,这个方法自己写也能做出来。
      可以有两种方案,一种是采用putVal的方法,因为很明显putVal是能够替换的,但是这里就涉及到了size,和modCount这两个field的变化了,也是要注意的。

另一种是getNode得到节点,然后替换,所以采用了getNode这个方法。

  1. //分为两种,一种是死切摆列的非要key和 value都匹配上,才换
  2. //另一种就是键匹配就换
  3. public boolean replace(K key, V oldValue, V newValue) {
  4. Node<K,V> e; V v;
  5. if ((e = getNode(hash(key), key)) != null &&
  6. ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
  7. e.value = newValue;
  8. afterNodeAccess(e);
  9. return true;
  10. }
  11. return false;
  12. }
  1. public V replace(K key, V value) {
  2. Node<K,V> e;
  3. if ((e = getNode(hash(key), key)) != null) {
  4. V oldValue = e.value;
  5. e.value = value;
  6. afterNodeAccess(e);
  7. return oldValue;
  8. }
  9. return null;
  10. }

可以看出jdk1.8考虑了更多的应用场景。

5.其他方法

  1. /**
  2. * Removes all of the mappings from this map.
  3. * The map will be empty after this call returns.
  4. */
  5. public void clear() {
  6. Node<K,V>[] tab;
  7. modCount++;
  8. if ((tab = table) != null && size > 0) {
  9. size = 0;
  10. for (int i = 0; i < tab.length; ++i)
  11. tab[i] = null;
  12. }
  13. }
  1. /**
  2. * Returns the number of key-value mappings in this map.
  3. *
  4. * @return the number of key-value mappings in this map
  5. */
  6. public int size() {
  7. return size;
  8. }
  1. /**
  2. * Returns <tt>true</tt> if this map contains no key-value mappings.
  3. *
  4. * @return <tt>true</tt> if this map contains no key-value mappings
  5. */
  6. public boolean isEmpty() {
  7. return size == 0;
  8. }

六.总结

通过分析HashMap源码,可以很好的领会Hash算法的实现原理,以及实现中的各种问题,同时对于链表的操作也是一个非常好的提高过程。

这里也发现,其实这次学习笔记,只是记录了最基本的增删改查,而红黑树,以及entryset,迭代器等等都还没有进行介绍,这也是留在后面有时间要重点介绍的。同时自己的Hash算法基础也要过过关了。

这是第一篇博客,是对自己学习过程的一个记录,也是一个督促。最后再感谢杭州.Mark的那篇博文,对自己分析HashMap有一个非常大的借鉴意义。

基于jdk1.8的HashMap源码学习笔记的更多相关文章

  1. 基于JDK1.8的LinkedList源码学习笔记

    LinkedList作为一种常用的List,是除了ArrayList之外最有用的List.其同样实现了List接口,但是除此之外它同样实现了Deque接口,而Deque是一个双端队列接口,其继承自Qu ...

  2. 基于JDK1.8的String源码学习笔记

    String,可能是学习Java一上来就学习的,经常用,但是却往往只是一知半解,甚至API有时也得现查.所以还是老规矩,倒腾源码. 一.java doc 这次首先关注String的doc,因为其实作为 ...

  3. Java集合(七)--基于jdk1.8的HashMap源码

    HashMap在开发中经常用,面试源码方面也会经常问到,在之前也多次了解过源码,今天算是复习一下,顺便好好总结一下,包括在后面有 相关面试题.本文不会对红黑树代码由太多深入研究,特别是删除方面太复杂, ...

  4. hashMap源码学习记录

    hashMap作为java开发面试最常考的一个题目之一,有必要花时间去阅读源码,了解底层实现原理. 首先,让我们看看hashMap这个类有哪些属性 // hashMap初始数组容量 static fi ...

  5. Hadoop源码学习笔记(4) ——Socket到RPC调用

    Hadoop源码学习笔记(4) ——Socket到RPC调用 Hadoop是一个分布式程序,分布在多台机器上运行,事必会涉及到网络编程.那这里如何让网络编程变得简单.透明的呢? 网络编程中,首先我们要 ...

  6. JUC源码学习笔记4——原子类,CAS,Volatile内存屏障,缓存伪共享与UnSafe相关方法

    JUC源码学习笔记4--原子类,CAS,Volatile内存屏障,缓存伪共享与UnSafe相关方法 volatile的原理和内存屏障参考<Java并发编程的艺术> 原子类源码基于JDK8 ...

  7. JDK源码学习笔记——LinkedHashMap

    HashMap有一个问题,就是迭代HashMap的顺序并不是HashMap放置的顺序,也就是无序. LinkedHashMap保证了元素迭代的顺序.该迭代顺序可以是插入顺序或者是访问顺序.通过维护一个 ...

  8. RocketMQ 源码学习笔记————Producer 是怎么将消息发送至 Broker 的?

    目录 RocketMQ 源码学习笔记----Producer 是怎么将消息发送至 Broker 的? 前言 项目结构 rocketmq-client 模块 DefaultMQProducerTest ...

  9. RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的?

    目录 RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的? 前言 项目结构 rocketmq-client 模块 DefaultMQProducerTest Roc ...

随机推荐

  1. Redis更新的正确方法

    原文(缓存更新的套路):看到好些人在写更新缓存数据代码时,先删除缓存,然后再更新数据库,而后续的操作会把数据再装载的缓存中.然而,这个是逻辑是错误的.试想,两个并发操作,一个是更新操作,另一个是查询操 ...

  2. WPF获取当前用户控件的父级窗体

    方式一.通过当前控件名获取父级窗体 Window targetWindow = Window.GetWindow(button); 方式二.通过当前控件获取父级窗体 Window parentWind ...

  3. 微软BI 之SSIS 系列 - 理解Data Flow Task 中的同步与异步, 阻塞,半阻塞和全阻塞以及Buffer 缓存概念

    开篇介绍 在 SSIS Dataflow 数据流中的组件可以分为 Synchronous 同步和 Asynchronous 异步这两种类型. 同步与异步 Synchronous and Asynchr ...

  4. jq 回车提交指定按钮

    $(this).keydown(function (e) { var key = window.event ? e.keyCode : e.which; ") { $("#Main ...

  5. zabbix用户管理

    zabbix用户管理,主要包括用户增删改查.用户报警媒介管理.用户权限管理. 安装完zabbix后,已经自带了两个用户Admin和Guests 超级管理员默认账号:Admin,密码:zabbix,这是 ...

  6. TinyMCE与Domino集成

    TinyMCE与Domino集成 一:TinyMCE简介 TinyMCE是一个轻量级的基于浏览器的所见即所得编辑器,由JavaScript写成.它对IE6+和Firefox1.5+都有着非常良好的支持 ...

  7. 如何查看Ubuntu下已安装包版本号

    之前的工作大部分在红帽企业版 下工作,查找安装包用rpm 很方便,但是由于这边大部分的服务器的运行环境是ubantu,补补. 在终端下也可以很方便查看已安装的软件包版本号,也能单独查看所需要的软件包是 ...

  8. PEM文件

    原文链接: http://blog.sina.com.cn/s/blog_489f88710100a59w.html OpenSSL 使用 PEM 文件格式存储证书和密钥.PEM 实质上是 Base6 ...

  9. 每帧创建一个item

    -- 加载列表测试 function UIBagController:onLoadTest() self.goodsprop = DB.getTable("goodsprop"); ...

  10. python列表中元素插入位置总结

    要完成的操作是把一个列表里的元素通过for循环添加到另外一个列表里,但是通过insert()方法添加到另外一个列表后却发现元素的位置与原始列表的颠倒了.如以下实例: li1 = ['] li2 = [ ...