JDK-In-Action-HashMap

HashMap

非线程安全的散列表实现,采用链表法解决Hash冲突.
优化点:

  • hash函数,避免key的hashcode()设计糟糕导致hash冲突严重
  • 冲突链表转红黑树,线性查找改进为对数查找
  • 2倍扩容, 简单.(相对于素数桶大小而言)

源码导读

初始构造容量大小

int tableSizeFor(int cap) 返回最接近cap的2的幂大小,例如 1,2,4,8,16...
指定初始容量或者从集合数据构造,都调用tableSizeFor()方法来确定threshold值.

元素大小阀值

int threshold; capacity * load factor , 元素大小超过该阀值需要扩容

负载因子

final float loadFactor; 容器会在size > threshold=(cap*loadFactor) 时扩容2倍
默认负载因子为 0.75f

底层数据结构

transient Node<K,V>[] table; 线性表, 通过 hash(key) & (cap-1) 来求索引位置

扩容步骤

final Node<K,V>[] resize()
关键设计点:
因为扩容是2倍扩容newCap=oldCap*2,所以新容量始终是2的幂.
可以观察到,一个Node在new数组中的索引值,要么不变,要么需要加oldCap,解释如下:

oldCap=16,16-1=15, 0 1 1 1 1
newCap=32,32-1=31, 1 1 1 1 1
-------------------------------------&
A_Node.hash        0 0 1 0 1
B_Node.hash        1 0 1 0 1

可以观察到仅当Node.hash值的高位是1,新的索引值才会发生变化,也就是B_Node.
B_Node 在old中的索引是 0101 , 在new中的索引为 1101=0101+oldCap
那么如何判断Node在新的table中索引是否需要加oldCap?
计算 (e.hash & oldCap) == 0 ? oldCap的低位为0,所以仅当e.hash的高位与oldCap二进制的高为均为1时结果才不为0
true=>A类节点
false=>B类节点

  • 对于没有hash冲突的key,重新计算索引位置
newTab[e.hash & (newCap - 1)] = e;
  • 对于红黑树的节点,按A类节点和B类节点进行分割(两类节点在新table中的索引不一样),如果新树太小则需要转换成链表形式
else if (e instanceof TreeNode)
    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  • 这一段代码用于处理有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) {//扩容后索引位置不变的key
        if (loTail == null)
            loHead = e;
        else
            loTail.next = e;
        loTail = e;
    }
    else {//扩容后索引位置需要+oldCap的key
        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;//新索引需要+oldCap
}

红黑树Hash冲突优化

//TODO

Hash函数-Hash扰动优化

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

因为HashMap使用的是长度为2的幂的散列表(扩容更容易,另一种方案是素数查找), 会遇到在低位上没有差异的hashcode的冲突. 对给定key的hashCode应用一个附加的散列函数,以防止质量较差的散列函数导致严重的hash冲突.

另见: https://stackoverflow.com/questions/15437345/java-a-prime-number-or-a-power-of-two-as-hashmap-size

Hash冲突优化-双向链表转红黑树

插入数据时,如果出现冲突,将扫描冲突索引的链表元素个数binCount , 如果 binCount >= TREEIFY_THRESHOLD - 1 则进行转化:
final void treeifyBin(Node<K,V>[] tab, int hash)

clear操作

size=0;
将底层 table 数组的每一个元素引用赋值null

API Example

元素操作之 Put

 Map<Integer, Object> map = new HashMap<>();

 //添加单个映射,会覆盖已存在的值
 Object old = map.put(1, 0);
 assertEquals(old, null);

 //返回被覆盖的值
 old = map.put(1, 0);
 assertEquals(old, 0);

 Map<Integer, Object> src = new HashMap<>();
 src.put(1, 1);
 src.put(99, 1);

 //批量添加映射,会覆盖已存在的值
 map.putAll(src);

 //添加映射,key不存在才添加
 old = map.putIfAbsent(1, 2);
 assertEquals(old, 1);

 old = map.putIfAbsent(2, 100);
 assertEquals(old, null);

元素操作之 Remove

 Map<Integer, Object> map = new HashMap<>();
 map.put(1, 1);
 map.put(2, 2);
 map.put(3, 3);
 map.put(4, 4);

 //移除指定key
 Object old = map.remove(0);
 assertEquals(old, null);


 old = map.remove(1);
 assertEquals(old, 1);

 //移除指定key,且指定value
 boolean remove = map.remove(2, 3);
 assertEquals(remove, false);

 remove = map.remove(2, 2);
 assertEquals(remove, true);

元素操作之 Replace

 Map<Integer, Object> map = new HashMap<>();
 map.put(1, 1);
 map.put(2, 2);

 map.replace(1, 10);
 map.replace(2, 2, 200);

 assertEquals(map, "{1=10, 2=200}");

元素操作之查找

 Map<Integer, Object> map = new HashMap<>();
 map.put(1, 1);
 map.put(2, 2);

 //键值访问
 Object value = map.get(1);
 assertEquals(value, 1);

 //键是否包含
 boolean containsKey = map.containsKey(1);
 assertEquals(containsKey, true);

 //值是否包含
 boolean containsValue = map.containsValue(2);
 assertEquals(containsKey, true);

构造函数之初始化容量和负载因子参数

 HashMap<Integer, Object> hashMap = new HashMap<>(128, 0.75f);
 println("容器仅在首次使用时才分配内存");
 hashMap.put(1, 1);
 assertEquals(hashMap, "{1=1}");
容器仅在首次使用时才分配内存

构造函数之拷贝其他 Map

 Map<Integer, Object> src = new HashMap<>();
 src.put(1, 1);
 HashMap<Integer, Object> hashMap = new HashMap<>(src);
 hashMap.put(2, 1);
 assertEquals(hashMap, "{1=1, 2=1}");

构造函数之空 Map

 HashMap<Integer, Object> hashMap = new HashMap<>();
 assertEquals(hashMap, "{}");

迭代操作之 Entry Set

 Map<Integer, Object> map = new HashMap<>();
 map.put(1, 1);
 map.put(2, 2);

 //key-value的集合,无序,对entrySet的操作会反应到源map
 Set<Map.Entry<Integer, Object>> entrySet = map.entrySet();
 Iterator<Map.Entry<Integer, Object>> iterator = entrySet.iterator();
 while (iterator.hasNext()) {
     Map.Entry<Integer, Object> next = iterator.next();
     println(next);
 }

 println("----OR----");

 for (Map.Entry entry : map.entrySet()) {
     println(entry);
 }
1=1
2=2
----OR----
1=1
2=2

迭代操作之 Foreach

 Map<Integer, Object> map = new HashMap<>();
 map.put(1, 1);
 map.put(2, 2);

 map.forEach((k, v) -> printTable(k, v));
1 | 1 | 
2 | 2 | 

迭代操作之 Key Set

 Map<Integer, Object> map = new HashMap<>();
 map.put(1, 1);
 map.put(2, 2);

 //key的集合,无序,对keySet的操作会反应到源map
 Set<Integer> keySet = map.keySet();
 assertEquals(keySet, "[1, 2]");

 //键移除会导致映射移除
 keySet.remove(1);
 Object value = map.get(1);
 assertEquals(value, null);

// keySet.add(3);//不支持的操作

 keySet.clear();
 assertEquals(map.isEmpty(), true);

引用

posted @ 2020-05-06 16:17  onion94  阅读(211)  评论(0编辑  收藏  举报