天天看点

HashMap源码分析

HashMap的历史

HashMap最早是在jdk1.2中开始出现的,一直到jdk1.7一直没有太大的变化。

但是到了jdk1.8突然进行了一个很大的改动。其中一个最显著的改动就是:

之前jdk1.7的存储结构是数组+链表,到了jdk1.8变成了数组+链表+红黑树。

另外,HashMap是非线程安全的,进行增删改操作的时候,是不能保证数据的一致性的。

HashMap源码分析

红黑树是一个自平衡的二叉查找树,也就是说红黑树的查找效率是非常的高,查找效率会从链表的o(n)降低为o(logn)。

HashMap名词介绍

名称 用途
initialCapacity HashMap 初始容量
loadFactor 负载因子
threshold 当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容

负载因子(loadFactor)

对于 HashMap 来说,负载因子是一个很重要的参数,该参数反应了 HashMap 桶数组的使用情况。

通过调节负载因子,可使 HashMap 时间和空间复杂度上有不同的表现。

当我们调低负载因子时,HashMap 所能容纳的键值对数量变少。

扩容时,重新将键值对存储新的桶数组里,键的键之间产生的碰撞会下降,链表长度变短。此时,HashMap 的增删改查等操作的效率将会变高,这里是典型的拿空间换时间。

相反,如果增加负载因子(负载因子可以大于1),HashMap 所能容纳的键值对数量变多,空间利用率高,但碰撞率也高。这意味着链表长度变长,效率也随之降低,这种情况是拿时间换空间。

至于负载因子怎么调节,这个看使用场景了。一般情况下,我们用默认值就可以了。

Node[] table

即哈希桶数组 ,Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。

Node[] table的初始化长度length(默认值是16),

Load factor为负载因子(默认值是0.75),

threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。

threshold = length * Load factor。

也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。

static class Node<K,V> implements Map.Entry<K,V> {             final int hash;             final K key;             V value;             Node<K,V> next;             Node(int hash, K key, V value, Node<K,V> next) {                 this.hash = hash;                 this.key = key;                 this.value = value;                 this.next = next;             }             public final K getKey()        { return key; }             public final V getValue()      { return value; }             public final String toString() { return key + "=" + value; }             public final int hashCode() {                 return Objects.hashCode(key) ^ Objects.hashCode(value);             }             public final V setValue(V newValue) {                 V oldValue = value;                 value = newValue;                 return oldValue;             }             public final boolean equals(Object o) {                 if (o == this)                     return true;                 if (o instanceof Map.Entry) {                     Map.Entry<?,?> e = (Map.Entry<?,?>)o;                     if (Objects.equals(key, e.getKey()) &&                         Objects.equals(value, e.getValue()))                         return true;                 }                 return false;        }         }           

源码解析

初始化 threshold 的方法

/**      * Returns a power of two size for the given target capacity.      */     static final int tableSizeFor(int cap) {         int n = cap - 1;         n |= n >>> 1;         n |= n >>> 2;         n |= n >>> 4;         n |= n >>> 8;         n |= n >>> 16;         return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;     }           

总结起来就一句话:找到大于或等于 cap 的最小2的幂。

put(K key, V value)

public V put(K key, V value) {             return putVal(hash(key), key, value, false, true);         }           
也就是说,put方法其实调用的是putVal方法。putVal方法有5个参数:     (1)第一个参数hash:调用了hash方法计算hash值     (2)第二个参数key:就是我们传入的key值,也就是例子中的张三     (3)第三个参数value:就是我们传入的value值,也就是例子中的20     (4)第四个参数onlyIfAbsent:也就是当键相同时,不修改已存在的值     (5)第五个参数evict :如果为false,那么数组就处于创建模式中,所以一般为true。           
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,                        boolean evict) {             Node<K,V>[] tab;	//定义的数组             Node<K,V> p;	//当前插入的节点             int n, i;             //第一部分             //如果table尚未初始化,则此处进行初始化数组,并赋值初始容量,重新计算阈值             if ((tab = table) == null || (n = tab.length) == 0)                 n = (tab = resize()).length;             //第二部分             //判断hash值,不冲突,new一个Node插入进去             if ((p = tab[i = (n - 1) & hash]) == null)                 tab[i] = newNode(hash, key, value, null);             //第三部分             //处理冲突,如果通过hash找到的位置有数据,发生碰撞             else {                 Node<K,V> e;                  K k;                 //第三部分--A                 //p是当前节点,如果需要插入的key和当前hash值指定下标的key一样,一样的直接替换旧值e                 if (p.hash == hash &&                     ((k = p.key) == key || (key != null && key.equals(k))))                     e = p;                 //第三部分--B                 //如果此时桶中数据类型为 treeNode,使用红黑树进行插入                 else if (p instanceof TreeNode)                     e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);                 //第三部分--C                 //此时桶中数据类型为链表                 else {                     //看这里的 .next,如果是数组,就会进去遍历                     //没有 就newNode                //化树的阈值,TREEIFY_THRESHOLD,所以不存在并且在链表末尾插入元素的时候                     for (int binCount = 0; ; ++binCount) {                         if ((e = p.next) == null) {                             p.next = newNode(hash, key, value, null);                             //如果链表过长,达到树化阈值,将链表转化成红黑树                             if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st                                 treeifyBin(tab, hash);                             break;                         }                         //如果链表中有新插入的节点位置数据不为空,则此时e 赋值为节点的值,跳出循环                         if (e.hash == hash &&                             ((k = e.key) == key || (key != null && key.equals(k))))                             break;                         p = e;                     }                 }                 //经过上面的循环后,如果e不为空,则说明上面插入的值已经存在于当前的hashMap中,                 //那么更新指定位置的键值对                 if (e != null) { // existing mapping for key                     V oldValue = e.value;                     if (!onlyIfAbsent || oldValue == null)                         e.value = value;                     afterNodeAccess(e);                     return oldValue;                 }             }             //第四部分,判断是否大于阈值,是否扩容             ++modCount;             if (++size > threshold)                 resize();             afterNodeInsertion(evict);             return null;         }           
HashMap源码分析
HashMap源码分析

代码看,put方法分为三种情况:

  • table尚未初始化,对数据进行初始化
  • table已经初始化,且通过hash算法找到下标所在的位置数据为空,直接将数据存放到指定位置
  • table已经初始化,且通过hash算法找到下标所在的位置数据不为空,发生hash冲突(碰撞),发生碰撞后,会执行以下操作:
    • 判断插入的key如果等于当前位置的key的话,将 e 指向该键值对
    • 如果此时桶中数据类型为 treeNode,使用红黑树进行插入
    • 如果是链表,则进行循环判断, 如果链表中包含该节点,跳出循环,如果链表中不包含该节点,则把该节点插入到链表末尾,同时,如果链表长度超过树化阈值(TREEIFY_THRESHOLD)且table容量超过最小树化容量(MIN_TREEIFY_CAPACITY),则进行链表转红黑树(由于table容量越小,越容易发生hash冲突,因此在table容量<MIN_TREEIFY_CAPACITY 的时候,如果链表长度>TREEIFY_THRESHOLD,会优先选择扩容,否则会进行链表转红黑树操作)

扩容前的小知识

怎么计算Hash值?

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

​ 与16异或的位运算。

​ 在数据结构中,我们处理hash冲突常使用的方法有:开发定址法、再哈希法、链地址法、建立公共溢出区。而hashMap中处理hash冲突的方法就是链地址法。

​ “链”,很容易想到,因为常用删除修改等操作,所以效率更高。

​ initialCapacity初始容量(你输入的最接近2的N次幂的数字),loadFactor负载因子(0.75,泊松分布)。

/* ---------------- Public operations -------------- */         /**          * Constructs an empty <tt>HashMap</tt> with the specified initial          * capacity and load factor.          *          * @param  initialCapacity the initial capacity          * @param  loadFactor      the load factor          * @throws IllegalArgumentException if the initial capacity is negative          *         or the load factor is nonpositive          */         public HashMap(int initialCapacity, float loadFactor) {             if (initialCapacity < 0)                 throw new IllegalArgumentException("Illegal initial capacity: " +                                                    initialCapacity);             if (initialCapacity > MAXIMUM_CAPACITY)                 initialCapacity = MAXIMUM_CAPACITY;       if (loadFactor <= 0 || Float.isNaN(loadFactor))                 throw new IllegalArgumentException("Illegal load factor: " +                                                    loadFactor);             this.loadFactor = loadFactor;             this.threshold = tableSizeFor(initialCapacity);         }         /**          * Constructs an empty <tt>HashMap</tt> with the specified initial          * capacity and the default load factor (0.75).          *          * @param  initialCapacity the initial capacity.          * @throws IllegalArgumentException if the initial capacity is negative.          */         public HashMap(int initialCapacity) {             this(initialCapacity, DEFAULT_LOAD_FACTOR);         }         /**          * Constructs an empty <tt>HashMap</tt> with the default initial capacity          * (16) and the default load factor (0.75).          */         public HashMap() {             this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted         }         /**          * Constructs a new <tt>HashMap</tt> with the same mappings as the          * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with          * default load factor (0.75) and an initial capacity sufficient to          * hold the mappings in the specified <tt>Map</tt>.          *          * @param   m the map whose mappings are to be placed in this map          * @throws  NullPointerException if the specified map is null          */         public HashMap(Map<? extends K, ? extends V> m) {             this.loadFactor = DEFAULT_LOAD_FACTOR;             putMapEntries(m, false);         }           

扩容 resize()

/**          * Initializes or doubles table size.  If null, allocates in          * accord with initial capacity target held in field threshold.          * Otherwise, because we are using power-of-two expansion, the          * elements from each bin must either stay at same index, or move          * with a power of two offset in the new table.          *          * @return the table          */     //初始化或加倍表大小。如果为空,则根据字段阈值中持有的初始容量目标进行分配。否则,因为我们使用的是 2 的幂扩展,所以每个 bin 中的元素必须保持相同的索引,或者在新表中以 2 的幂的偏移量移动。         final Node<K,V>[] resize() {             Node<K,V>[] oldTab = table;             int oldCap = (oldTab == null) ? 0 : oldTab.length;             int oldThr = threshold;		//threshold 临界点             int newCap, newThr = 0;             //第一部分,扩容             //如果超过了直接将阈值设置为整数的最大值,没有,则,通过移位操作扩大2倍。             if (oldCap > 0) {                 //如果旧的容量已近达到最大值,则不再扩容,阈值直接设置为最大值                 if (oldCap >= MAXIMUM_CAPACITY) {                     threshold = Integer.MAX_VALUE;                     return oldTab;                 }                 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&                          oldCap >= DEFAULT_INITIAL_CAPACITY)                     //如果旧的容量不小于默认的初始容量,则进行扩容,容量扩张为原来的二倍                     newThr = oldThr << 1; // double threshold             }             //第二部分             //如果初始化过了,就使用已经有的阈值             //不然,就重新初始化一个             else if (oldThr > 0) // initial capacity was placed in threshold                 newCap = oldThr;             //threshold 和 table 皆未初始化情况,此处即为首次进行初始化             //也就在此处解释了构造方法中没有对threshold 和 初始容量进行赋值的问题             else {               // zero initial threshold signifies using defaults                 //如果阈值为零,表示使用默认的初始化值                 //这种情况在调用无参构造的时候会出现,此时使用默认的容量和阈值                 newCap = DEFAULT_INITIAL_CAPACITY;                 //此处阈值即为 threshold=initialCapacity*loadFactor                 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);             }             // newThr 为 0 时,按阈值计算公式进行计算,容量*负载因子             if (newThr == 0) {            float ft = (float)newCap * loadFactor;                 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?                           (int)ft : Integer.MAX_VALUE);             }              //更新阈值             threshold = newThr;             //更新数组桶             @SuppressWarnings({"rawtypes","unchecked"})             Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];             table = newTab;             //第三部分             //将旧的数据迁移到新的数组             //如果之前的数组桶里面已经存在数据,由于table容量发生变化,hash值也会发生变化,需要重新计算下标             if (oldTab != null) {                 //遍历旧的数据                 for (int j = 0; j < oldCap; ++j) {                     Node<K,V> e;                     //如果指定下标下有数据                     if ((e = oldTab[j]) != null) {                         //将指定下标数据置空                         oldTab[j] = null;                         //指定下标只有一个数据                         if (e.next == null)                             //直接将数据存放到新计算的hash值下标下                             newTab[e.hash & (newCap - 1)] = e;                         //如果是TreeNode数据结构                         else if (e instanceof TreeNode)                             ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);                         //对于链表,数据结构                         else { // preserve order                             //如果是链表,重新计算hash值,根据新的下标重新分组                             Node<K,V> loHead = null, loTail = null;                             Node<K,V> hiHead = null, hiTail = null;                             Node<K,V> next;                             //逐个转移数组                             do {                                 next = e.next;                                 if ((e.hash & oldCap) == 0) {                                     if (loTail == null)                                         loHead = e;                                     else                                         loTail.next = e;                                     loTail = e;                                 }                                 else {                                     if (hiTail == null)                                         hiHead = e;                                     else                                         hiTail.next = e;                                     hiTail = e;                                 }                             } while ((e = next) != null);                             if (loTail != null) {                                 loTail.next = null;                                 newTab[j] = loHead;                             }                             if (hiTail != null) {                                 hiTail.next = null;                                 newTab[j + oldCap] = hiHead;                             }                         }                     }                 }             }             return newTab;         }           
HashMap源码分析

resize分为以下几步:

  • 首先先判断当前table是否进行过初始化,如果没有进行过初始化,此处就解决了调用无参构造方法时候,threshold和initialCapacity 未初始化的问题,如果已经初始化过了,则进行扩容,容量为原来的二倍
  • 扩容后创建新的table,并对所有的数据进行遍历
    • 如果新计算的位置数据为空,则直接插入
    • 如果新计算的位置为链表,则通过hash算法重新计算下标,对链表进行分组
    • 如果是红黑树,则需要进行拆分操作

get方法,查找

public V get(Object key) {             Node<K,V> e;             return (e = getNode(hash(key), key)) == null ? null : e.value;         }         final Node<K,V> getNode(int hash, Object key) {             Node<K,V>[] tab;              Node<K,V> first, e;              int n; K k;             if ((tab = table) != null && (n = tab.length) > 0 &&                 (first = tab[(n - 1) & hash]) != null) {                 //根据hash算法找到对应位置的第一个数据,如果是指定的key,则直接返回                 if (first.hash == hash && // always check first node                     ((k = first.key) == key || (key != null && key.equals(k))))                     return first;                 if ((e = first.next) != null) {                     //如果该节点为红黑树,则通过树进行查找                     if (first instanceof TreeNode)                         return ((TreeNode<K,V>)first).getTreeNode(hash, key);                     //如果该节点是链表,则遍历查找到数据                     do {                         if (e.hash == hash &&                             ((k = e.key) == key || (key != null && key.equals(k))))                             return e;                     } while ((e = e.next) != null);                 }             }             return null;         }           

hash源码

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

这段代码叫做扰动函数,也是hashMap中的hash运算,主要分为下面几步:

  • key.hashCode(),获取key的hashCode值,如果不进行重写的话返回的是根据内存地址得到的一个int值
  • key.hashCode() 获取到的hashcode无符号右移16位并和元hashCode进行^ ,这样做的目的是为了让高位与低进行混合,让两者都参与运算,以便让hash值分布更加均匀

取模运算 (n - 1) & hash

在hashMap的代码中,在很多地方都会看到类似的代码:

first = tab[(n - 1) & hash])           

hash算法中,为了使元素分布的更加均匀,很多都会使用取模运算,

在hashMap中并没有使用hash%n这样进行取模运算,而是使用(n - 1) & hash进行代替,

原因是在计算机中,&的效率要远高于%。

需要注意的是,     只有容量为2的n次幂的时候,(n - 1) & hash 才能等效hash%n,     这也是hashMap 初始化初始容量时,无论传入任何值,都会通过tableSizeFor(int cap) 方法转化成2的n次幂的原因,     这种巧妙的设计真的很令人惊叹。           

有兴趣的可以看一下一位大佬写的文章:由HashMap哈希算法引出的求余%和与运算&转换问题

remove方法,删除

public V remove(Object key) {             Node<K,V> e;             return (e = removeNode(hash(key), key, null, false, true)) == null ?                 null : e.value;         }         final Node<K,V> removeNode(int hash, Object key, Object value,                                    boolean matchValue, boolean movable) {             Node<K,V>[] tab; Node<K,V> p; int n, index;             //根据key和key的hash值,查找到对应的元素             if ((tab = table) != null && (n = tab.length) > 0 &&                 (p = tab[index = (n - 1) & hash]) != null) {                 Node<K,V> node = null, e; K k; V v;                 if (p.hash == hash &&                     ((k = p.key) == key || (key != null && key.equals(k))))                     node = p;                 else if ((e = p.next) != null) {                     if (p instanceof TreeNode)                         node = ((TreeNode<K,V>)p).getTreeNode(hash, key);                     else {                         do {                             if (e.hash == hash &&                                 ((k = e.key) == key ||                                  (key != null && key.equals(k)))) {                                 node = e;                                 break;                             }                             p = e;                         } while ((e = e.next) != null);                     }                 }                 //如果查找的了元素node,移除即可                 if (node != null && (!matchValue || (v = node.value) == value ||                                      (value != null && value.equals(v)))) {                     //如果是TreeNode,通过树进行移除                     if (node instanceof TreeNode)                         ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);                     //如果是第一个节点,移除第一个节点,将index下标的位置指向第二个节点                     else if (node == p)                         tab[index] = node.next;                     else                         //如果不是链表的第一个节点,则移除该节点                         p.next = node.next;                     ++modCount;                     --size;                     afterNodeRemoval(node);                     return node;                 }             }             return null;         }           

链表树化

static final int TREEIFY_THRESHOLD = 8;     /**      * 当桶数组容量小于该值时,优先进行扩容,而不是树化      */     static final int MIN_TREEIFY_CAPACITY = 64;     static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {         TreeNode<K,V> parent;  // red-black tree links         TreeNode<K,V> left;         TreeNode<K,V> right;         TreeNode<K,V> prev;    // needed to unlink next upon deletion         boolean red;         TreeNode(int hash, K key, V val, Node<K,V> next) {             super(hash, key, val, next);         }     }     /**      * 将普通节点链表转换成树形节点链表      */     final void treeifyBin(Node<K,V>[] tab, int hash) {         int n, index;          Node<K,V> e;         // 桶数组容量小于 MIN_TREEIFY_CAPACITY,优先进行扩容而不是树化         if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)             resize();         else if ((e = tab[index = (n - 1) & hash]) != null) {             // hd 为头节点(head),tl 为尾节点(tail)             TreeNode<K,V> hd = null, tl = null;             do {                 // 将普通节点替换成树形节点                 TreeNode<K,V> p = replacementTreeNode(e, null);                 if (tl == null)                     hd = p;                 else {                     p.prev = tl;                     tl.next = p;                 }                 tl = p;             } while ((e = e.next) != null);  // 将普通链表转成由树形节点链表             if ((tab[index] = hd) != null)                 // 将树形链表转换成红黑树                 hd.treeify(tab);         }     }     TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {         return new TreeNode<>(p.hash, p.key, p.value, next);     }           

在扩容过程中,树化要满足两个条件:

  1. 链表长度大于等于 TREEIFY_THRESHOLD
  2. 桶数组容量大于等于 MIN_TREEIFY_CAPACITY

红黑树链化与拆分

链化

final Node<K,V> untreeify(HashMap<K,V> map) {         Node<K,V> hd = null, tl = null;         // 遍历 TreeNode 链表,并用 Node 替换         for (Node<K,V> q = this; q != null; q = q.next) {             // 替换节点类型             Node<K,V> p = map.replacementNode(q, null);             if (tl == null)                 hd = p;             else                 tl.next = p;             tl = p;         }         return hd;     }     Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {         return new Node<>(p.hash, p.key, p.value, next);     }           

拆分

// 红黑树转链表阈值     static final int UNTREEIFY_THRESHOLD = 6;     final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {         TreeNode<K,V> b = this;         // Relink into lo and hi lists, preserving order         TreeNode<K,V> loHead = null, loTail = null;         TreeNode<K,V> hiHead = null, hiTail = null;         int lc = 0, hc = 0;         /*           * 红黑树节点仍然保留了 next 引用,故仍可以按链表方式遍历红黑树。          * 下面的循环是对红黑树节点进行分组,与上面类似          */         for (TreeNode<K,V> e = b, next; e != null; e = next) {             next = (TreeNode<K,V>)e.next;             e.next = null;             if ((e.hash & bit) == 0) {                 if ((e.prev = loTail) == null)                     loHead = e;                 else                     loTail.next = e;                 loTail = e;                 ++lc;             }             else {                 if ((e.prev = hiTail) == null)                     hiHead = e;                 else                     hiTail.next = e;                 hiTail = e;                 ++hc;             }         }         if (loHead != null) {             // 如果 loHead 不为空,且链表长度小于等于 6,则将红黑树转成链表             if (lc <= UNTREEIFY_THRESHOLD)                 tab[index] = loHead.untreeify(map);             else {                 tab[index] = loHead;                 /*                   * hiHead == null 时,表明扩容后,                  * 所有节点仍在原位置,树结构不变,无需重新树化                  */                 if (hiHead != null)                      loHead.treeify(tab);             }         }         // 与上面类似         if (hiHead != null) {             if (hc <= UNTREEIFY_THRESHOLD)                 tab[index + bit] = hiHead.untreeify(map);             else {                 tab[index + bit] = hiHead;                 if (loHead != null)                     hiHead.treeify(tab);             }         }     }           

在将普通链表转成红黑树时,HashMap 通过两个额外的引用 next 和 prev 保留了原链表的节点顺序。

这样再对红黑树进行重新映射时,完全可以按照映射链表的方式进行。这样就避免了将红黑树转成链表后再进行映射,无形中提高了效率。

从源码上可以看得出,重新映射红黑树的逻辑和重新映射链表的逻辑基本一致。不同的地方在于,重新映射后,会将红黑树拆分成两条由 TreeNode 组成的链表。

如果链表长度小于 UNTREEIFY_THRESHOLD,则将链表转换成普通链表。否则根据条件重新将 TreeNode 链表树化。

HashMap源码分析

被 transient 所修饰 table 变量

其实这个关键字的作用很好理解,

就是简单的一句话:将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化。

transient Node<K,V>[] table;         /**          * Holds cached entrySet(). Note that AbstractMap fields are used          * for keySet() and values().          */         transient Set<Map.Entry<K,V>> entrySet;         /**          * The number of key-value mappings contained in this map.          */         transient int size;         /**          * The number of times this HashMap has been structurally modified          * Structural modifications are those that change the number of mappings in          * the HashMap or otherwise modify its internal structure (e.g.,          * rehash).  This field is used to make iterators on Collection-views of          * the HashMap fail-fast.  (See ConcurrentModificationException).          */         transient int modCount;           

HashMap 并没有使用默认的序列化机制,而是通过实现

readObject/writeObject

两个方法自定义了序列化的内容。

序列化 talbe 存在着两个问题:

  1. table 多数情况下是无法被存满的,序列化未使用的部分,浪费空间
  2. 同一个键值对在不同 JVM 下,所处的桶位置可能是不同的,在不同的 JVM 下反序列化 table 可能会发生错误。

第二个问题解释:HashMap 的

get/put/remove

等方法第一步就是根据 hash 找到键所在的桶位置,

但如果键没有覆写 hashCode 方法,计算 hash 时最终调用 Object 中的 hashCode 方法。但 Object 中的 hashCode 方法是 native 型的,不同的 JVM 下,可能会有不同的实现,产生的 hash 可能也是不一样的。也就是说同一个键在不同平台下可能会产生不同的 hash,此时再对在同一个 table 继续操作,就会出现问题。

总结

  • HashMap 底层数据结构在JDK1.7之前是由数组+链表组成的,1.8之后又加入了红黑树;链表长度小于8的时候,发生Hash冲突后会增加链表的长度,当链表长度大于8的时候,会先判读数组的容量,如果容量小于64会先扩容(原因是数组容量越小,越容易发生碰撞,因此当容量过小的时候,首先要考虑的是扩容),如果容量大于64,则会将链表转化成红黑树以提升效率。
  • hashMap 的容量是2的n次幂,无论在初始化的时候传入的初始容量是多少,最终都会转化成2的n次幂,这样做的原因是为了在取模运算的时候可以使用&运算符,而不是%取余,可以极大的提上效率,同时也降低hash冲突。
  • HashMap是非线程安全的,在多线程的操作下会存在异常情况(如形成闭环(1.7),1.8已修复闭环问题,但仍不安全),可以使用HashTable或者ConcurrentHashMap进行代替

HashMap这样的知识点还有很多,一篇文章是说不完的,至于更多的玄机需要我们一起去探索。

参考链接:

HashMap源码分析(jdk1.8,保证你能看懂) - 知乎 (zhihu.com)

Java 8系列之重新认识HashMap - 知乎 (zhihu.com)

有大量时间看这个,特别全: HashMap 源码详细分析(JDK1.8) - SegmentFault 思否

HashMap源码分析 —— 一篇文章搞定HashMap面试 (juejin.cn)