Java 集合类 Map 笔记

Hashtable、HashMap、TreeMap 三者区别

比较点HashtableHashMapTreeMap
key不能为null可以为 null
value不能为 null可以为 null
key一定情况下可以为 null
有序性无序无序有序
容量初始 11初始 16,且一定为 2 n 2^n 2n
扩容2n+12n
线程安全性线程安全线程不安全
hash 值计算底层是模运算,效率较低底层是移位运算

HashMap

HashMap 可以存储 null 的 key 和 value,作为 null 的 key 只能有一个,null 值可以有多个。

key 的唯一性也要求 null 只能有一个,这种理解很正常;如果把 null 值当作与其他普通值一样看待,null 允许多个也不足为奇。

null 的 hash 值为 0,这是源码规定的:

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

其他 value 的 hash 值与 hashCode 有关

JDK 1.8 的 HashMap 由数组 + 链表组成。

数组是 HashMap 的主体:

transient Node<K,V>[] table;

链表是为了解决哈希冲突而存在(拉链法):

p.next = newNode(hash, key, value, null);

使用 hash 值,通过 (n - 1) & hash 得到元素在数组中的索引:

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

hash 值也可以直接使用 key.hashCode(),但是并没有,而是进行了 hash 方法进行扰动,防止一些糟糕的 hashCode(),而且也满足了 null 值的插入需求:

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

一个数与 0 做 “异或” 运算等于这个数与 0 做 “或” 运算,所以高 4 字节保留,低 4 字节与高 4 字节做异或运算。

当链表长度大于 8 时,会调用 treeifyBin() 方法。但这个方法也并非一定会将链表树化,只有当数组长度大于等于 64 的情况下,才会执行红黑树转换。

loadFactory 加载因子:控制数组存放数据的疏密程度,loadFactory 越趋近于 1,那么数组存放的数据越多,越密;loadFactory 越小(越趋近于0),存放的数据越系数。

默认值 0.75f 是官方给出的比较好的临界值

threshold:capacity * loadFactory,当 size >= threshold 时,需要考虑对数组扩容。

putVal

如果定位到的数组位置没有元素,就直接插入。

如果定位到的数组位置有元素,就要和插入的 key 比较。

if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))
    // 找到该节点
    e = p;
else if (p instanceof TreeNode)
    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
    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;
        }
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            break;
        p = e;
    }
}

这段判断逻辑为为什么必须要增加 p.hash == hash 的逻辑? 个人觉得是 hash 值是由 hashCode() 方法计算得到,同时以 hashCode 和 equals 方法约束两个对象是否相同。

if (p.hash == hash &&
    ((k = p.key) == key || (key != null && key.equals(k))))

resize

如果你的底层数组大小已经 >= MAXIMUM_CAPACITY,那么 threshold 将会设置为 Integer.MAX_VALUE,这时候的 threshold 是比 capacity 大的。这体现在:1、当你初始化设置为一个 >= MAXIMUM_CAPACITY 的值,2. 或者运行时扩容达到 >= MAXIMUM_CAPACITY

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
    	// 如果旧数组的大小已经达到最大值,不再扩容,返回旧数组
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 尝试扩容,如果扩容之后的大小没有达到最大值,且旧数组 oldCap >= DEFAULT_INITIAL_CAPACITY
        // 那么,获得新的 new threshold
        // 这里 newCap 和 newThr 都是双倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 比如,从 12 变为 24
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        // 如果构造 HashMap 的时候传入了 initCapacity,那么这里 oldThr 就是 initCapacity
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
    	// 第一次 put 的时候会进入该逻辑
        newCap = DEFAULT_INITIAL_CAPACITY; // 默认大小 16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 阈值 12
    }
    if (newThr == 0) {
    	// 1. 如果是自定义 initCapacity,第一次会计算一下 newThr
    	// 2. 如果是自定义 initCapacity,大小偏小,大小没有达到 DEFAULT_INITIAL_CAPACITY,例如:1,2,4,8 此类,也会计算 newThr
        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;
    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)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    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;
}

TreeMap

TreeMap 关于 key 为 null 的说明:TreeMap 初始化时可以传入 Comparator 参数,但也可以不传入。如果没有传入,在 put 元素时,默认使用 key 实现的 Comparable 进行比较然后按顺序存储(如果 key 没有实现 Comparable,在 ClassCast 的时候直接抛出异常),当然,key 如果为 null,则调用方法 compare 直接空指针异常。

但是,如果传入 Comparator,那么在 Comparator 的方法 compare 中,小心地判断了 key 为 null 的情况,也是可以将 key 存储为 null 的。

ConcurrentHashMap

initTable

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
    	// sizeCtl 控制全局的 size 变量
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        
        // 多线程尝试将 sizectl 从 sc 置为 -1,表示一种上锁
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
            	// 锁标记还原
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

sizeCtl :默认为0,用来控制table的初始化和扩容操作
-1 代表table正在初始化
-N 表示 -N 对应的二进制的低 16 位数值为 M,此时有 M - 1 个线程进行扩容。

为什么 HashMap 允许 value 为 null? 而 ConcurrentHashMap 不允许 value 为 null?

无论哪种 Map,假设允许 value 存储 null 值,那么在调用 get 方法的时候,有两种可能性:

  • 该 key 没有在 Map 中映射过
  • 该 key 在 Map 中存在对应的 value,但是值为 null

但是,HashMap 在单线程的正确使用场景下,我们可以使用 containsKey(key) 方法来区分是否包含该 key

如果在多线程 ConcurrentHashMap 下,使用 get 方法得到 null,但我们不知道该 null 是不存在映射,还是 value 为 null,因此我们会调用 containsKey 方法判断,假设 get 与 containsKey 方法之间有其他线程进行了 put,我们就无法判断了。因此,ConcurrentHashMap 干脆直接禁止 value 为 null。

不过,建议,HashMap 也不要存储 null 值,就像 HashSet 底层的 HashMap 也会将 value 存储为一个标记对象,而不是一个 null。

至于 ConcurrentHashMap 为什么不支持 key 为 null,可能只能归咎于源码本身并不支持。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罐装面包

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值