Java集合框架之 Java HashMap 源码解析

开发 后端
继上一篇文章Java集合框架综述后,今天正式开始分析具体集合类的代码,首先以既熟悉又陌生的HashMap开始。

继上一篇文章Java集合框架综述后,今天正式开始分析具体集合类的代码,首先以既熟悉又陌生的HashMap开始。

签名(signature)

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

可以看到HashMap继承了

  • 标记接口Cloneable,用于表明HashMap对象会重写java.lang.Object#clone()方法,HashMap实现的是浅拷贝(shallow copy)。

  • 标记接口Serializable,用于表明HashMap对象可以被序列化

比较有意思的是,HashMap同时继承了抽象类AbstractMap与接口Map,因为抽象类AbstractMap的签名为

  1. public abstract class AbstractMap<K,V> implements Map<K,V> 

Stack Overfloooow上解释到:

在语法层面继承接口Map是多余的,这么做仅仅是为了让阅读代码的人明确知道HashMap是属于Map体系的,起到了文档的作用

AbstractMap相当于个辅助类,Map的一些操作这里面已经提供了默认实现,后面具体的子类如果没有特殊行为,可直接使用AbstractMap提供的实现。

Cloneable接口

  1. <code>It's evil, don't use it. </code> 

Cloneable这个接口设计的非常不好,最致命的一点是它里面竟然没有clone方法,也就是说我们自己写的类完全可以实现这个接口的同时不重写clone方法。

关于Cloneable的不足,大家可以去看看《Effective Java》一书的作者给出的理由,在所给链接的文章里,Josh Bloch也会讲如何实现深拷贝比较好,我这里就不在赘述了。

Map接口

Eclipse中的outline面板可以看到Map接口里面包含以下成员方法与内部类:

 

Java HashMap 源码解析
Map_field_method

可以看到,这里的成员方法不外乎是“增删改查”,这也反映了我们编写程序时,一定是以“数据”为导向的。

 

上篇文章讲了Map虽然并不是Collection,但是它提供了三种“集合视角”(collection views),与下面三个方法一一对应:

  • Set<K> keySet(),提供key的集合视角

  • Collection<V> values(),提供value的集合视角

  • Set<Map.Entry<K, V>> entrySet(),提供key-value序对的集合视角,这里用内部类Map.Entry表示序对

AbstractMap抽象类

AbstractMapMap中的方法提供了一个基本实现,减少了实现Map接口的工作量。

举例来说:

如果要实现个不可变(unmodifiable)的map,那么只需继承AbstractMap,然后实现其entrySet方法,这个方法返回的set不支持add与remove,同时这个set的迭代器(iterator)不支持remove操作即可。

相反,如果要实现个可变(modifiable)的map,首先继承AbstractMap,然后重写(override)AbstractMap的put方法,同时实现entrySet所返回set的迭代器的remove方法即可。

设计理念(design concept)

哈希表(hash table)

HashMap是一种基于哈希表(hash table)实现的map,哈希表(也叫关联数组)一种通用的数据结构,大多数的现代语言都原生支持,其概念也比较简单:key经过hash函数作用后得到一个槽(buckets或slots)的索引(index),槽中保存着我们想要获取的值,如下图所示

 

Java HashMap 源码解析
hash table demo

很容易想到,一些不同的key经过同一hash函数后可能产生相同的索引,也就是产生了冲突,这是在所难免的。
所以利用哈希表这种数据结构实现具体类时,需要:

 

  • 设计个好的hash函数,使冲突尽可能的减少

  • 其次是需要解决发生冲突后如何处理。

后面会重点介绍HashMap是如何解决这两个问题的。

HashMap的一些特点

  • 线程非安全,并且允许key与value都为null值,HashTable与之相反,为线程安全,key与value都不允许null值。

  • 不保证其内部元素的顺序,而且随着时间的推移,同一元素的位置也可能改变(resize的情况)

  • put、get操作的时间复杂度为O(1)。

  • 遍历其集合视角的时间复杂度与其容量(capacity,槽的个数)和现有元素的大小(entry的个数)成正比,所以如果遍历的性能要求很高, 不要把capactiy设置的过高或把平衡因子(load factor,当entry数大于capacity*loadFactor时,会进行resize,reside会导致key进行rehash)设置的过 低。

  • 由于HashMap是线程非安全的,这也就是意味着如果多个线程同时对一hashmap的集合试图做迭代时有结构的上改变(添加、删除entry,只改变entry的value的值不算结构改变),那么会报ConcurrentModificationException,专业术语叫fail-fast,尽早报错对于多线程程序来说是很有必要的。

  • Map m = Collections.synchronizedMap(new HashMap(...)); 通过这种方式可以得到一个线程安全的map。

源码剖析

首先从构造函数开始讲,HashMap遵循集合框架的约束,提供了一个参数为空的构造函数与有一个参数且参数类型为Map的构造函数。除此之外,还提供了两个构造函数,用于设置HashMap的容量(capacity)与平衡因子(loadFactor)。

 

  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.     threshold = initialCapacity; 
  12.     init(); 
  13. public HashMap(int initialCapacity) { 
  14.     this(initialCapacity, DEFAULT_LOAD_FACTOR); 
  15. public HashMap() { 
  16.     this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); 
  17.  
  18. 从代码上可以看到,容量与平衡因子都有个默认值,并且容量有个***值 
  19.  
  20. /** 
  21. * The default initial capacity - MUST be a power of two. 
  22. */ 
  23. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4// aka 16 
  24. /** 
  25. * The maximum capacity, used if a higher value is implicitly specified 
  26. * by either of the constructors with arguments. 
  27. * MUST be a power of two <= 1<<30. 
  28. */ 
  29. static final int MAXIMUM_CAPACITY = 1 << 30
  30. /** 
  31. * The load factor used when none specified in constructor. 
  32. */ 
  33. static final float DEFAULT_LOAD_FACTOR = 0.75f; 

可以看到,默认的平衡因子为0.75,这是权衡了时间复杂度与空间复杂度之后的***取值(JDK说是***的),过高的因子会降低存储空间但是查找(lookup,包括HashMap中的put与get方法)的时间就会增加。

这里比较奇怪的是问题:容量必须为2的指数倍(默认为16),这是为什么呢?解答这个问题,需要了解HashMap中哈希函数的设计原理。

哈希函数的设计原理

  1. /** 
  2.   * Retrieve object hash code and applies a supplemental hash function to the 
  3.   * result hash, which defends against poor quality hash functions.  This is 
  4.   * critical because HashMap uses power-of-two length hash tables, that 
  5.   * otherwise encounter collisions for hashCodes that do not differ 
  6.   * in lower bits. Note: Null keys always map to hash 0, thus index 0. 
  7.   */ 
  8. final int hash(Object k) { 
  9.      int h = hashSeed; 
  10.      if (0 != h && k instanceof String) { 
  11.          return sun.misc.Hashing.stringHash32((String) k); 
  12.      } 
  13.      h ^= k.hashCode(); 
  14.      // This function ensures that hashCodes that differ only by 
  15.      // constant multiples at each bit position have a bounded 
  16.      // number of collisions (approximately 8 at default load factor). 
  17.      h ^= (h >>> 20) ^ (h >>> 12); 
  18.      return h ^ (h >>> 7) ^ (h >>> 4); 
  19. /** 
  20.   * Returns index for hash code h. 
  21.   */ 
  22. static int indexFor(int h, int length) { 
  23.      // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; 
  24.      return h & (length-1); 

看到这么多位操作,是不是觉得晕头转向了呢,还是搞清楚原理就行了,毕竟位操作速度是很快的,不能因为不好理解就不用了。

网上说这个问题的也比较多,我这里根据自己的理解,尽量做到通俗易懂。

在哈希表容量(也就是buckets或slots大小)为length的情况下,为了使每个key都能在冲突最小的情况下映射到[0,length)(注意是左闭右开区间)的索引(index)内,一般有两种做法:

  1. 让length为素数,然后用hashCode(key) mod length的方法得到索引

  2. 让length为2的指数倍,然后用hashCode(key) & (length-1)的方法得到索引

HashTable用的是方法1,HashMap用的是方法2。

因为本篇主题讲的是HashMap,所以关于方法1为什么要用素数,我这里不想过多介绍,大家可以看这里

重点说说方法2的情况,方法2其实也比较好理解:

因为length为2的指数倍,所以length-1所对应的二进制位都为1,然后在与hashCode(key)做与运算,即可得到[0,length)内的索引

但是这里有个问题,如果hashCode(key)的大于length的值,而且hashCode(key)的二进制位的低位变化不大,那么冲突就会很多,举个例子:

Java中对象的哈希值都32位整数,而HashMap默认大小为16,那么有两个对象那么的哈希值分别为:0xABAB00000xBABA0000,它们的后几位都是一样,那么与16异或后得到结果应该也是一样的,也就是产生了冲突。

造成冲突的原因关键在于16限制了只能用低位来计算,高位直接舍弃了,所以我们需要额外的哈希函数而不只是简单的对象的hashCode方法了。

具体来说,就是HashMap中hash函数干的事了

首先有个随机的hashSeed,来降低冲突发生的几率

然后如果是字符串,用了sun.misc.Hashing.stringHash32((String) k);来获取索引值

***,通过一系列无符号右移操作,来把高位与低位进行异或操作,来降低冲突发生的几率

右移的偏移量20,12,7,4是怎么来的呢?因为Java中对象的哈希值都是32位的,所以这几个数应该就是把高位与低位做异或运算,至于这几个数是如何选取的,就不清楚了,网上搜了半天也没统一且让人信服的说法,大家可以参考下面几个链接:

HashMap.Entry

HashMap中存放的是HashMap.Entry对象,它继承自Map.Entry,其比较重要的是构造函数

  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.     Entry(int h, K k, V v, Entry<K,V> n) { 
  7.         value = v; 
  8.         next = n; 
  9.         key = k; 
  10.         hash = h; 
  11.     } 
  12.     // setter, getter, equals, toString 方法省略 
  13.     public final int hashCode() { 
  14.         //用key的hash值与上value的hash值作为Entry的hash值 
  15.         return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); 
  16.     } 
  17.     /** 
  18.      * This method is invoked whenever the value in an entry is 
  19.      * overwritten by an invocation of put(k,v) for a key k that's already 
  20.      * in the HashMap. 
  21.      */ 
  22.     void recordAccess(HashMap<K,V> m) { 
  23.     } 
  24.     /** 
  25.      * This method is invoked whenever the entry is 
  26.      * removed from the table. 
  27.      */ 
  28.     void recordRemoval(HashMap<K,V> m) { 
  29.     } 

可以看到,Entry实现了单向链表的功能,用next成员变量来级连起来。

介绍完Entry对象,下面要说一个比较重要的成员变量

/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
//HashMap内部维护了一个为数组类型的Entry变量table,用来保存添加进来的Entry对象
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

你也许会疑问,Entry不是单向链表嘛,怎么这里又需要个数组类型的table呢?

我翻了下之前的算法书,其实这是解决冲突的一个方式:链地址法(开散列法),效果如下:

 

Java HashMap 源码解析
链地址法处理冲突得到的散列表

就是相同索引值的Entry,会以单向链表的形式存在

 

链地址法的可视化

网上找到个很好的网站,用来可视化各种常见的算法,很棒。瞬间觉得国外大学比国内的强不知多少倍。

下面的链接可以模仿哈希表采用链地址法解决冲突,大家可以自己去玩玩

get操作

get操作相比put操作简单,所以先介绍get操作

  1. public V get(Object key) { 
  2.     //单独处理key为null的情况 
  3.     if (key == null
  4.         return getForNullKey(); 
  5.     Entry<K,V> entry = getEntry(key); 
  6.     return null == entry ? null : entry.getValue(); 
  7. private V getForNullKey() { 
  8.     if (size == 0) { 
  9.         return null
  10.     } 
  11.     //key为null的Entry用于放在table[0]中,但是在table[0]冲突链中的Entry的key不一定为null 
  12.     //所以需要遍历冲突链,查找key是否存在 
  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. final Entry<K,V> getEntry(Object key) { 
  19.     if (size == 0) { 
  20.         return null
  21.     } 
  22.     int hash = (key == null) ? 0 : hash(key); 
  23.     //首先定位到索引在table中的位置 
  24.     //然后遍历冲突链,查找key是否存在 
  25.     for (Entry<K,V> e = table[indexFor(hash, table.length)]; 
  26.          e != null
  27.          e = e.next) { 
  28.         Object k; 
  29.         if (e.hash == hash && 
  30.             ((k = e.key) == key || (key != null && key.equals(k)))) 
  31.             return e; 
  32.     } 
  33.     return null

put操作(含update操作)

因为put操作有可能需要对HashMap进行resize,所以实现略复杂些

  1. private void inflateTable(int toSize) { 
  2.     //辅助函数,用于填充HashMap到指定的capacity 
  3.     // Find a power of 2 >= toSize 
  4.     int capacity = roundUpToPowerOf2(toSize); 
  5.     //threshold为resize的阈值,超过后HashMap会进行resize,内容的entry会进行rehash 
  6.     threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); 
  7.     table = new Entry[capacity]; 
  8.     initHashSeedAsNeeded(capacity); 
  9. /** 
  10. * Associates the specified value with the specified key in this map. 
  11. * If the map previously contained a mapping for the key, the old 
  12. * value is replaced. 
  13. */ 
  14. public V put(K key, V value) { 
  15.     if (table == EMPTY_TABLE) { 
  16.         inflateTable(threshold); 
  17.     } 
  18.     if (key == null
  19.         return putForNullKey(value); 
  20.     int hash = hash(key); 
  21.     int i = indexFor(hash, table.length); 
  22.     //这里的循环是关键 
  23.     //当新增的key所对应的索引i,对应table[i]中已经有值时,进入循环体 
  24.     for (Entry<K,V> e = table[i]; e != null; e = e.next) { 
  25.         Object k; 
  26.         //判断是否存在本次插入的key,如果存在用本次的value替换之前oldValue,相当于update操作 
  27.         //并返回之前的oldValue 
  28.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { 
  29.             V oldValue = e.value; 
  30.             e.value = value; 
  31.             e.recordAccess(this); 
  32.             return oldValue; 
  33.         } 
  34.     } 
  35.     //如果本次新增key之前不存在于HashMap中,modCount加1,说明结构改变了 
  36.     modCount++; 
  37.     addEntry(hash, key, value, i); 
  38.     return null
  39. void addEntry(int hash, K key, V value, int bucketIndex) { 
  40.     //如果增加一个元素会后,HashMap的大小超过阈值,需要resize 
  41.     if ((size >= threshold) && (null != table[bucketIndex])) { 
  42.         //增加的幅度是之前的1倍 
  43.         resize(2 * table.length); 
  44.         hash = (null != key) ? hash(key) : 0
  45.         bucketIndex = indexFor(hash, table.length); 
  46.     } 
  47.     createEntry(hash, key, value, bucketIndex); 
  48. void createEntry(int hash, K key, V value, int bucketIndex) { 
  49.     //首先得到该索引处的冲突链Entries,有可能为null,不为null 
  50.     Entry<K,V> e = table[bucketIndex]; 
  51.     //然后把新的Entry添加到冲突链的开头,也就是说,后插入的反而在前面(***次还真没看明白) 
  52.     //需要注意的是table[bucketIndex]本身并不存储节点信息, 
  53.     //它就相当于是单向链表的头指针,数据都存放在冲突链中。 
  54.     table[bucketIndex] = new Entry<>(hash, key, value, e); 
  55.     size++; 
  56. //下面看看HashMap是如何进行resize,庐山真面目就要揭晓了 
  57. void resize(int newCapacity) { 
  58.     Entry[] oldTable = table; 
  59.     int oldCapacity = oldTable.length; 
  60.     //如果已经达到***容量,那么就直接返回 
  61.     if (oldCapacity == MAXIMUM_CAPACITY) { 
  62.         threshold = Integer.MAX_VALUE; 
  63.         return
  64.     } 
  65.     Entry[] newTable = new Entry[newCapacity]; 
  66.     //initHashSeedAsNeeded(newCapacity)的返回值决定了是否需要重新计算Entry的hash值 
  67.     transfer(newTable, initHashSeedAsNeeded(newCapacity)); 
  68.     table = newTable; 
  69.     threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); 
  70. /** 
  71. * Transfers all entries from current table to newTable. 
  72. */ 
  73. void transfer(Entry[] newTable, boolean rehash) { 
  74.     int newCapacity = newTable.length; 
  75.     //遍历当前的table,将里面的元素添加到新的newTable中 
  76.     for (Entry<K,V> e : table) { 
  77.         while(null != e) { 
  78.             Entry<K,V> next = e.next; 
  79.             if (rehash) { 
  80.                 e.hash = null == e.key ? 0 : hash(e.key); 
  81.             } 
  82.             int i = indexFor(e.hash, newCapacity); 
  83.             e.next = newTable[i]; 
  84.             //***这两句用了与put放过相同的技巧 
  85.             //将后插入的反而在前面 
  86.             newTable[i] = e; 
  87.             e = next; 
  88.         } 
  89.     } 
  90. /** 
  91. * Initialize the hashing mask value. We defer initialization until we 
  92. * really need it. 
  93. */ 
  94. final boolean initHashSeedAsNeeded(int capacity) { 
  95.     boolean currentAltHashing = hashSeed != 0
  96.     boolean useAltHashing = sun.misc.VM.isBooted() && 
  97.             (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); 
  98.     //这里说明了,在hashSeed不为0或满足useAltHash时,会重算Entry的hash值 
  99.     //至于useAltHashing的作用可以参考下面的链接 
  100.     // http://stackoverflow.com/questions/29918624/what-is-the-use-of-holder-class-in-hashmap 
  101.     boolean switching = currentAltHashing ^ useAltHashing; 
  102.     if (switching) { 
  103.         hashSeed = useAltHashing 
  104.             ? sun.misc.Hashing.randomHashSeed(this
  105.             : 0
  106.     } 
  107.     return switching; 

remove操作

  1. public V remove(Object key) { 
  2.     Entry<K,V> e = removeEntryForKey(key); 
  3.     //可以看到删除的key如果存在,就返回其所对应的value 
  4.     return (e == null ? null : e.value); 
  5. final Entry<K,V> removeEntryForKey(Object key) { 
  6.     if (size == 0) { 
  7.         return null
  8.     } 
  9.     int hash = (key == null) ? 0 : hash(key); 
  10.     int i = indexFor(hash, table.length); 
  11.     //这里用了两个Entry对象,相当于两个指针,为的是防治冲突链发生断裂的情况 
  12.     //这里的思路就是一般的单向链表的删除思路 
  13.     Entry<K,V> prev = table[i]; 
  14.     Entry<K,V> e = prev; 
  15.     //当table[i]中存在冲突链时,开始遍历里面的元素 
  16.     while (e != null) { 
  17.         Entry<K,V> next = e.next; 
  18.         Object k; 
  19.         if (e.hash == hash && 
  20.             ((k = e.key) == key || (key != null && key.equals(k)))) { 
  21.             modCount++; 
  22.             size--; 
  23.             if (prev == e) //当冲突链只有一个Entry时 
  24.                 table[i] = next; 
  25.             else 
  26.                 prev.next = next; 
  27.             e.recordRemoval(this); 
  28.             return e; 
  29.         } 
  30.         prev = e; 
  31.         e = next; 
  32.     } 
  33.     return e; 

到现在为止,HashMap的增删改查都介绍完了。
一般而言,认为HashMap的这四种操作时间复杂度为O(1),因为它hash函数性质较好,保证了冲突发生的几率较小。

HashMap的序列化

介绍到这里,基本上算是把HashMap中一些核心的点讲完了,但还有个比较严重的问题:保存Entry的table数组为transient的,也就是说在进行序列化时,并不会包含该成员,这是为什么呢?

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

为了解答这个问题,我们需要明确下面事实:

  • Object.hashCode方法对于一个类的两个实例返回的是不同的哈希值

我们可以试想下面的场景:

我们在机器A上算出对象A的哈希值与索引,然后把它插入到HashMap中,然后把该HashMap序列化后,在机器B上重新算对象的哈希值与索引,这与机器A上算出的是不一样的,所以我们在机器B上get对象A时,会得到错误的结果。

所以说,当序列化一个HashMap对象时,保存Entry的table是不需要序列化进来的,因为它在另一台机器上是错误的。

因为这个原因,HashMap重现了writeObjectreadObject 方法

  1. private void writeObject(java.io.ObjectOutputStream s) 
  2.     throws IOException 
  3.     // Write out the threshold, loadfactor, and any hidden stuff 
  4.     s.defaultWriteObject(); 
  5.  
  6.     // Write out number of buckets 
  7.     if (table==EMPTY_TABLE) { 
  8.         s.writeInt(roundUpToPowerOf2(threshold)); 
  9.     } else { 
  10.        s.writeInt(table.length); 
  11.     } 
  12.  
  13.     // Write out size (number of Mappings) 
  14.     s.writeInt(size); 
  15.  
  16.     // Write out keys and values (alternating) 
  17.     if (size > 0) { 
  18.         for(Map.Entry<K,V> e : entrySet0()) { 
  19.             s.writeObject(e.getKey()); 
  20.             s.writeObject(e.getValue()); 
  21.         } 
  22.     } 
  23.  
  24. private static final long serialVersionUID = 362498820763181265L; 
  25.  
  26. private void readObject(java.io.ObjectInputStream s) 
  27.      throws IOException, ClassNotFoundException 
  28.     // Read in the threshold (ignored), loadfactor, and any hidden stuff 
  29.     s.defaultReadObject(); 
  30.     if (loadFactor <= 0 || Float.isNaN(loadFactor)) { 
  31.         throw new InvalidObjectException("Illegal load factor: " + 
  32.                                            loadFactor); 
  33.     } 
  34.  
  35.     // set other fields that need values 
  36.     table = (Entry<K,V>[]) EMPTY_TABLE; 
  37.  
  38.     // Read in number of buckets 
  39.     s.readInt(); // ignored. 
  40.  
  41.     // Read number of mappings 
  42.     int mappings = s.readInt(); 
  43.     if (mappings < 0
  44.         throw new InvalidObjectException("Illegal mappings count: " + 
  45.                                            mappings); 
  46.  
  47.     // capacity chosen by number of mappings and desired load (if >= 0.25) 
  48.     int capacity = (int) Math.min( 
  49.                 mappings * Math.min(1 / loadFactor, 4.0f), 
  50.                 // we have limits... 
  51.                 HashMap.MAXIMUM_CAPACITY); 
  52.  
  53.     // allocate the bucket array; 
  54.     if (mappings > 0) { 
  55.         inflateTable(capacity); 
  56.     } else { 
  57.         threshold = capacity; 
  58.     } 
  59.  
  60.     init();  // Give subclass a chance to do its thing. 
  61.  
  62.     // Read the keys and values, and put the mappings in the HashMap 
  63.     for (int i = 0; i < mappings; i++) { 
  64.         K key = (K) s.readObject(); 
  65.         V value = (V) s.readObject(); 
  66.         putForCreate(key, value); 
  67.     } 
  68. private void putForCreate(K key, V value) { 
  69.     int hash = null == key ? 0 : hash(key); 
  70.     int i = indexFor(hash, table.length); 
  71.  
  72.     /** 
  73.      * Look for preexisting entry for key.  This will never happen for 
  74.      * clone or deserialize.  It will only happen for construction if the 
  75.      * input Map is a sorted map whose ordering is inconsistent w/ equals. 
  76.      */ 
  77.     for (Entry<K,V> e = table[i]; e != null; e = e.next) { 
  78.         Object k; 
  79.         if (e.hash == hash && 
  80.             ((k = e.key) == key || (key != null && key.equals(k)))) { 
  81.             e.value = value; 
  82.             return
  83.         } 
  84.     } 
  85.  
  86.     createEntry(hash, key, value, i); 

简单来说,在序列化时,针对Entry的key与value分别单独序列化,当反序列化时,再单独处理即可。

总结

在总结完HashMap后,发现这里面一些核心的东西,像哈希表的冲突解决,都是算法课上学到,不过由于“年代久远”,已经忘得差不多了,我觉得忘

  • 一方面是由于时间久不用

  • 另一方面是由于本身没理解好

平时多去思考,这样在遇到一些性能问题时也好排查。

还有一点就是我们在分析某些具体类或方法时,不要花太多时间一些细枝末节的边界条件上,这样很得不偿失,倒不是说这么边界条件不重要,程序的bug往往就是边界条件没考虑周全导致的。

只是说我们可以在理解了这个类或方法的总体思路后,再来分析这些边界条件。

如果一开始就分析,那真是丈二和尚——摸不着头脑了,随着对它工作原理的加深,才有可能理解这些边界条件的场景。

 

 

责任编辑:王雪燕 来源: ImportNew
相关推荐

2015-08-10 15:12:27

Java实例源码分析

2015-06-15 10:32:44

Java核心源码解读

2015-09-11 09:40:35

Java集合框架

2021-09-16 15:08:08

鸿蒙HarmonyOS应用

2021-03-11 07:27:22

Java 集合数据

2023-10-09 07:57:14

JavaJCF

2009-06-29 16:50:27

Java集合框架

2023-09-05 10:16:02

Java框架

2015-09-16 09:10:27

Java源码解析

2022-07-19 20:04:31

NAPI模块鸿蒙

2012-03-12 15:36:29

Java框架

2017-01-05 15:13:03

Java数组算法解释

2021-04-12 07:34:03

Java集合框架

2020-12-01 15:00:20

Java 基础

2012-11-06 11:07:59

jQueryJSjQuery框架

2024-01-18 08:31:22

go实现gorm框架

2011-07-11 11:02:12

JAVA集合框架

2012-04-26 10:52:52

Java数组集合

2019-07-22 09:59:20

Java框架集合

2020-01-09 09:56:47

Java集合框架
点赞
收藏

51CTO技术栈公众号