菜鸟看源码之HashMap

xiaoxiao2021-02-28  103

先扯点别的:听说拳皇97界的大魔王老K退役了,不知道以后还能不能见到那么犀利的大门。最近感觉自己的拳皇水平有点提升,应该能排到100多线的水平吧。

基于JDK1.8

先看一下HashMap的继承结构图 类介绍:

基于哈希表的Map接口实现。该实现提供了所有可选的映射操作,允许 null 值 和 null 键。HashMap不保证键值对的顺序;特别是,它不保证键值对的顺序随着时间的推移保持不变。

假设散列函数在桶之间正确地分散元素,该实现为基本操作( get 和 put )提供了常数级别的性能。对集合视图的迭代需要与哈希表的“容量”(桶的数量)加上其大小(键值映射的数量)成比例的时间。 因此,如果迭代性能很重要,则不要将初始容量设置得太高(或负载因子太低)。

有两个参数影响哈希表的性能:初始容量和负载因子。初始容量是哈希表中桶的数量,初始容量只是哈希表在创建时候的容量。负载因子用来衡量在哈希表的容量自动增加之前哈希表能够达到的饱和程度。当哈希表中的键值对的数量超过了负载因子和当前哈希表容量的乘积的时候,哈希表就会rehash,哈希表的容量大约会增加到原来的两倍。

作为一般规则,默认负载因子(0.75)在时间和空间成本之间提供了良好的平衡。如果负载因子较高,可以减少空间开销,但是会增加查找时间(反映到HashMap的大多数操作,包括 get 和 set )。在设置HashMap的初始化容量的时候,应该考虑到HashMap中的要存放的键值对的数量和负载因子,来减少rehash操作的次数。如果初始容量大于键值对的最大数量除以负载因子,那么就不需要rehash操作。

如果有很多键值对要被存储到HashMap中,那么在创建HashMap实例的时候指定一个足够大的初始化容量,在存储过程中效率更高,因为HashMap不需要rehash来增加容量。注意,使用具有相同hashCode的键毫无疑问会降低HashMap的性能。为了改善影响,当键是可比较的(Comparable)HashMap可能会使用这些键的比较顺序来帮助改善性能。(可是我没有找到哪里使用了Comparable)

HashMap不是线程安全的,如果多个线程同时访问HashMap而且至少有一个线程会改变HashMap的结构,那么必须由外部保证线程同步。(一个结构修改是添加或者删除一个或多个映射的操作,仅仅修改HashMap中已存在的一个键值对的值并不是结构修改)这通常通过在自然封装HashMap的某个对象上进行同步来实现。如果没有这样的对象存在,HashMap应该使用Collections.synchronizedMap 来包装。这个操作最好在创建HashMap对象的时候进行,以避免对当前HashMap对象意外的非同步的操作。Map m = Collections.synchronizedMap(new HashMap(...));

HashMap的所有集合视图方法返回的迭代器是快速失败的:在迭代器被创建之后,除了通过迭代器自己的remove方法之外的任何其他方法导致HashMap发生结构修改的时候,迭代器会抛出一个ConcurrentModificationException。因此,在并发修改的情况下,迭代器快速而干净地失败,避免在将来造成未确定时间的任意的,非确定性行为的风险。

请注意,无法保证迭代器的快速失败行为一般来说,在存在不同步的并发修改时,不可能做出任何硬性保证。快速失败迭代器会尽最大努力抛出ConcurrentModificationException。因此,编写依赖于此异常的程序以确保其正确性是错误的:迭代器的快速失败行为应该仅用于检测错误。

Collections#synchronizedMap()方法

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) { return new SynchronizedMap<>(m); }

返回的SynchronizedMap是线程安全的,因为它的每个方法都加了锁。 SynchronizedMap部分方法

final Object mutex; // Object on which to synchronize public int size() { synchronized (mutex) {return m.size();} } public boolean isEmpty() { synchronized (mutex) {return m.isEmpty();} }

HashMap 的存储结构:数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下如所示。

HashMap的部分成员变量

//序列化id private static final long serialVersionUID = 362498820763181265L; //默认初始容量16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //最大容量 static final int MAXIMUM_CAPACITY = 1 << 30; //默认的负载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; //树化链表节点的阈值 static final int TREEIFY_THRESHOLD = 8; //把树转化成普通链表的阈值 static final int UNTREEIFY_THRESHOLD = 6; //树化链表的最小容量。如果HashMap的容量小于小于这个值,则先进行扩容而不是树化链表。 static final int MIN_TREEIFY_CAPACITY = 64; //存储数据的Node数组,长度必须是2的幂。 transient Node<K,V>[] table; //entrySet transient Set<Map.Entry<K,V>> entrySet; //table中Node的数量 transient int size; //hashmap 对象被结构化修改的次数 transient int modCount; //负载因子 final float loadFactor; // 所能容纳的key-value对极限 ,容量乘以负载因子所得结果,如果key-value的 数量等于该值, //则调用resize方法,扩大容量,同时修改threshold的值。 int threshold;

Node 节点的数据结构:是一个单链表

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; } }

使用HashMap的时候通常会使用无参构造函数来实例化一个HashMap。

/** *使用默认的初始容量(16)构造一个空的HashMap,默认的负载因子是(0.75) */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // 其他的成员变量使用默认值 }

实例化HashMap的时候也可以指定初始容量(initialCapacity)和负载因子(loadFactor)

public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }

只指定初始容量的构造函数会在内部调用两个参数的构造函数,如下所示。

/** *使用指定的初始化容量和负载因子构造一个空的HashMap */ public HashMap(int initialCapacity, float loadFactor) { //... //给负载因子赋值为指定的大小 this.loadFactor = loadFactor; //调用tableSizeFor方法为threshold 赋值 this.threshold = tableSizeFor(initialCapacity); }

tableSizeFor(int cap)方法会返回一个大于或者等于cap的一个值,并且这个值必须是2的幂,比如32,64,等等。

通过上面的构造函数看出,内部就是为了给loadFactor和threshold赋值的,这个时候存储数据的Node<K,V>[] table还是空的。

HashMap确定哈希桶数组索引位置的方法 这里的Hash算法就是三步:取key的hashCode值、高位运算、位与运算。

//取key的hashCode值、高位运算 static final int hash(Object key) { int h; //这就是为什么最多只允许一条记录的键为null的原因,因为key为null计算出来的hash值是0 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } //位与运算,n为table的长度 (n - 1) & hash

然后就是HashMap的put(K key, V value)方法

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

把一个键值对key-newValue 放入到map中,如果这个map中已经包含一个键值对key-oldValue那么oldValue会被newValue替换,返回oldValue。如果不存在旧的键值对,就直接把key-newValue 放入到map中,返回null。内部通过putVal实现。

/** * * @param hash 键的hash值 * @param key 键 * @param value 要放入的value值 * @param onlyIfAbsent 如果为true,不改变已存在的value。 * @param evict 如果为false,table处在创建模式 * @return 键对应的旧的值,如果没有,返回null。 */ final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n; int i; //第一步:判断如果table为null或者table的长度为空就先初始化table if ((tab = table) == null || (n = tab.length) == 0) //初始化table n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) //如果i=((n - 1) & hash)位置上没有元素,即p为null //就直接把元素放入tab[i]的位置上 tab[i] = newNode(hash, key, value, null); else { //如果p不为null Node<K,V> e; K k; //以下条件判断key是否相等 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) //如果p是一个TreeNode,就把要put的元素加入到树中 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //如果p不是一个TreeNode,说明tab[i]位置上的元素是存放在链表里面的, //就把要put的元素加入到链表尾部,或者更新链表中某个位置上的元素的value for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { //把要put的元素加入到链表尾部 p.next = newNode(hash, key, value, null); //tab[i]位置上的链表中的元素个数超过TREEIFY_THRESHOLD //就通过treeifyBin方法把tab[i]位置上的链表变成一颗红黑树 //或者增加tab的容量 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //或者更新链表中某个位置上的元素的value if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //这里是真正更新value的地方。 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) //新值替换旧值 e.value = value; afterNodeAccess(e); //返回旧的值 return oldValue; } } //如果是新加入了一个元素,而不是更新了某个旧值,就把修改次数加1 ++modCount; //table的size加1如果超出了容量,就把table的长度变为原来的两倍 if (++size > threshold) resize(); //这方法是用来给LinkedHashMap做一些后期操作,HashMap中是空实现。 afterNodeInsertion(evict); return null; }

整理一下putVal方法的逻辑。

首先判断table是不是为空,或者table的长度是不是为0。如果满足其中一个条件就调用resize()方法来初始化table。计算出哈希桶数组索引位置(i = (n - 1) & hash])。如果i位置上没有元素即p为null,就直接把待放入元素放入 i 位置上。如果 i 位置上的元素p不为null,就判断hash是否等于p的哈希值,key值和p的key值是否相等,如果相等,那说明这个放入操作只要用value值替换p元素的value值即可,返回被替换的value值。如果条件3不满足,就判断p元素是否是一个TreeNode,如果是的话,说明当前 i 位置上是一个红黑树,那就从树中查找,如果在树中找到有一个元素 q , 如果q的key的hash值等于hash,同时 q 的key值等于key,那就用value更新 q 的value。否则就把待放入元素放入树中。如果条件 4 不满足,说明说明当前 i 位置上是一个链表,就遍历这个链表,如果在链表中发现有一个元素 q 的key的哈希值等于hash,同时 q 的键值等于key,就更新 q 的value,返回oldValue,结束。否则就把待放入的元素插入到链表尾部。插入完成后,检测当前链表的长度是否超过了TREEIFY_THRESHOLD,如果超过了,就把链表转化成一个红黑树。

上面提到的resize()方法做了两件事

若果table为null,或者table的长度为0,就初始化table。初始化容量大小为16,threshold为12。如果table不为null,就把table的长度增加两倍,原来table中的元素要么待在原来的位置index上,要么会移动到(index+ 2 n − 1 2^{n-1} 2n1)的位置上。(index 表示元素原来的位置, 2 n 2^{n} 2n 是HashMap的容量)。

如上图所示,HashMap长度为16。当HashMap长度增加到32的时候,原来在index=5的位置上的元素会移动到 2 4 2^{4} 24+5=21的位置上。

11111 &10101 =10101

结果是21。

HashMap的resize方法
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; //table的长度 int oldCap = (oldTab == null) ? 0 : oldTab.length; //将threshold赋值给oldThr int oldThr = threshold; //新的HashMap容量和扩容阈值 int newCap, newThr = 0; if (oldCap > 0) {//table不为null,这时是扩大table的容量 //table长度超过最大值就不再扩充了 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } //没超过最大值,如果table长度*2小于MAXIMUM_CAPACITY //同时table长度大于等于16 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; //threshold变为原来的两倍 } //如果table为null,说明此时是第一次初始化table, //如果我们实例化HashMap的时候指定了初始容量 initialCapacity, //那么会调用tableSizeFor(initialCapacity)计算table的容量, //并将计算好的容量赋值给threshold。 //所以此时threshold保存的是table的初始容量。threshold的值可能为0 else if (oldThr > 0) newCap = oldThr; else {//threshold为0,使用默认值初始化 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } //如果newThr == 0,使用我们指定的loadFactor重新计算 if (newThr == 0) { //确保新的扩容阈值不超过Integer.MAX_VALUE float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } //将新的扩容阈值赋值给threshold threshold = newThr; Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab;//将table指向扩容后的newTab if (oldTab != null) { // 把每个bucket都移动到新的buckets中 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null)//只有一个节点 newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // 注释1处,保持链表的顺序不变 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; //这时候oldCap是原来table的length,不是(length-1)了 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; }

上面方法的注释1处,用来保证链表的顺序不变。

// preserve order HashMap.Node<K, V> loHead = null, loTail = null; HashMap.Node<K, V> hiHead = null, hiTail = null; HashMap.Node<K, V> next; do { next = e.next; if ((e.hash & oldCap) == 0) {//注释1处, if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else {//注释2处 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { //注释3处 loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { //注释4处 hiTail.next = null; newTab[j + oldCap] = hiHead; }

举例分析一下。 场景1: 比如此时HashMap的长度是16,链表中所有元素的hash值是00101。

10000 &00101 =00000

那么此时满足注释1处的条件。即 e.hash & oldCap) == 0 。while循环完以后的结果就是注释3处。如下图所示。 场景2: 比如此时HashMap的长度是16,链表中所有元素的hash值是10101。

10000 &10101 =10000

那么此时满足注释2处的条件 。while循环完以后的结果就是注释4处。如下图所示。

场景3: 比如此时HashMap的长度是16,链表中部分元素(比如a,c)的hash值是00101,部分元素比如(b,d)的hash值是10101。那么循环完毕后的结果如下图所示。

上面提到的treeifyBin方法

如果table的size太小(小于64)就调用resize方法增加table的容量,否则就把链表变成一颗红黑树。至于怎么变的先不管。

final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { 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); } }

HashMap的putAll方法,把一个集合里面的所有键值对,都放入当前的HashMap中,内部通过putMapEntries实现。

public void putAll(Map<? extends K, ? extends V> m) { putMapEntries(m, true); } final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) { int s = m.size(); if (s > 0) { if (table == null) { // pre-size float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY); if (t > threshold) threshold = tableSizeFor(t); } else if (s > threshold) resize(); for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } } }

首先判断,如果当前hashMap的table为null,就先初始化threshold。如果table不为null,如果要加入的map的size大于当前hashMap的table的最大容量threshold,就调用resize()方法增加table的长度。最后循环遍历通过putVal方法把map中的每个键值对都放入当前hashMap的table中。

HashMap get方法,返回key对应的value,如果不存在对应的value,返回null;内部通过getNode实现。

public V get(Object key) { Node<K,V> e; //返回节点的value值或者null; return (e = getNode(hash(key), key)) == null ? null : e.value; } /** * * @param hash 键的哈希值 * @param key 键 * @return 返回找到的节点或者null。 */ 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) {//存在key对应的value 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值找出对应存储位置上的节点first ,如果first就是要找的节点就返回first。否则如果对应存储节点上是红黑树,就从红黑树里面找。是链表,就从链表中查找,找到就返回相应的Node,找不到返回null;

HashMapde 的containsKey方法,内部通过getNode方法实现,如果table中存在key-value对,返回true,否则返回false。

public boolean containsKey(Object key) { return getNode(hash(key), key) != null; }

HashMap的containsValue方法,内部遍历整个table,查找是否有元素的value值和要查找的value值相等,存在返回true,否则返回false。

public boolean containsValue(Object value) { Node<K,V>[] tab; V v; if ((tab = table) != null && size > 0) { for (int i = 0; i < tab.length; ++i) { for (Node<K,V> e = tab[i]; e != null; e = e.next) { if ((v = e.value) == value || (value != null && value.equals(v))) return true; } } } return false; }

HashMap的remove方法,内部通过removeNode实现,如果成功删除了键值对,返回被删除的value,否则返回null;

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; 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; //如果hash对应的位置上的元素p就是要删除的节点 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null) { //如果hash值对应的位置上存储的是一个红黑树,就从树里查找 if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { //如果hash值对应的位置上存储的是链表,就遍历链表查找。 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)))) { if (node instanceof TreeNode) //node是一个红黑树中的节点,删除树中的节点 ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p) //如果node就是p节点,意思就是tab[index]位置上就是要删除的node, //那么就把node的next节点赋值给tab[index]。 tab[index] = node.next; else //要删除的节点node是链表中的某个节点, //就让node的上一个节点p指向node的next,即可完成删除 p.next = node.next; //修改次数加1 ++modCount; //table中的键值对的数量减1 --size; afterNodeRemoval(node); //返回删除的node return node; } } return null; }

HashMap的两个replace方法,就是根据key值找到相应的节点,更新节点的value,更新成功返回true。如果找不到key对应的节点,返回false。

public V replace(K key, V value) { Node<K,V> e; if ((e = getNode(hash(key), key)) != null) { V oldValue = e.value; e.value = value; afterNodeAccess(e); return oldValue; } return null; } @Override public boolean replace(K key, V oldValue, V newValue) { Node<K,V> e; V v; if ((e = getNode(hash(key), key)) != null && ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) { e.value = newValue; afterNodeAccess(e); return true; } return false; }

HashMap的clear方法,把table的每一项都置为null。增加修改次数,size置为0。

public void clear() { Node<K,V>[] tab; //修改次数加1 modCount++; if ((tab = table) != null && size > 0) { size = 0; for (int i = 0; i < tab.length; ++i) tab[i] = null; } }
一些问题

HashMap什么时候会进行rehash?

jdk7中才需要rehash,jdk8已经不需要了。

HashMap什么时候会进行扩容?

当哈希表中的条目的数量超过了负载因子和当前哈希表容量的乘积的时候,哈希表会扩容,哈希表的容量大约会增加到原来的两倍。

HashMap的初始容量设置成多少比较合适呢?

(初始容量x负载因子)略大于我们想要放入的键值对的数量比较合适,这样就不需要扩容操作。

结合源码说说HashMap在高并发场景中为什么会出现死循环?

这个问题在jdk7中会出现。在扩容以后将Entry数组的元素拷贝到新的Entry数组的时候,使用了单链表的头插入方式,如果多个线程同时将Entry数组的元素拷贝到新的Entry数组可能会造成链表成环,然后我们在获取数据的时候会出现无限循环。具体可以参Java 8系列之重新认识HashMap , 此处不做详细论证。

JDK1.8中对HashMap做了哪些性能优化?

当链表长度超过8的时候,将链表转化成一颗红黑树,这样查找效率更高。不再需要rehash。

HashMap和HashTable有何不同?

HashMap不是线程安全的。HashTable是线程安全的,每个方法都加了锁。

HashMap 和 ConcurrentHashMap 的区别?

HashMap不是线程安全的。ConcurrentHashMap是线程安全的,在并发场景下应该使用ConcurrentHashMap。

6.为什么ConcurrentHashMap中的链表转红黑树的阀值是8?

红黑树的平均查找长度是log(n),如果长度为8,平均查找长度为log(8)=3,链表的平均查找长度为n/2,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,而log(6)=2.6,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

小结

(1) 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值(初始化容量可以考虑设置为 expectedSize / 0.75F + 1.0F),避免map进行频繁的扩容。

(2) 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。

(3) HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap。

(4) JDK1.8引入红黑树大程度优化了HashMap的性能。

参考链接

Java你可能不知道的事(3)HashMapHashMap源码分析(基于JDK8)Java 8系列之重新认识HashMapJava HashMap工作原理及实现为什么Map桶中个数超过8才转为红黑树为什么HashMap链表长度超过8会转成树结构
转载请注明原文地址: https://www.6miu.com/read-28716.html

最新回复(0)