解析HashMap(二)
一、概述
JDK1.8中对HashMap
做了很大的优化,其中最重要的优化就是其底层实现将原来的“数组+链表”改为“数组+链表+红黑树”。下面通过源码来分析HashMap
。
二、源码分析:
1. 常量:
1 | /* 默认的数组的长度 或者说 默认是buckets/bins的长度 16 */ |
2. 属性:
有些属性设置为transient
是与序列化有关。
transient 关键字
Java序列化会把某一个类存储以文件形式存储在物理空间,但是以文件形式存储某些信息时,容易涉及到安全问题,因为数据位于Java运行环境之外, 不在Java安全机制的控制之中。对于这些需要保密的字段,不应保存在永久介质中 ,或者不应简单地不加处理地保存下来 ,为了保证安全性。 应该在这些字段前加上transient关键字。它的意思是临时的,即不会随类一起序列化到本地,所以当还原后,这个关键字定义的变量也就不再存在。
1 | /* 用于存节点的数组(节点会hash到table中的某一个index) */ |
3. 内部类
Node<K, V>
继承了Map.Entry<K, V>
类。
1 | static class Node<K, V> implements Map.Entry<K, V> { |
4. 构造函数:
我发现构造函数中,并没有initialCapacity
这个属性,仅仅使用其来判断容量异常,通过这个属性求threshold
,之后没有使用它,数组的长度没有声明,所以如何给数组初始化多大长度呢?
是通过resize()
来初始化数组长度的。
1 | /* 传入容量和负载因子两个参数 */ |
tableSizeFor(int cap)
这个方法和JDK1.7中的roundUpToPowerOf2()
中使用的Integer.highestOneBit(int i)
方法相似。
返回值一定是2的幂,该方法是返回大于等于cap
的最小2的幂。
1 | static final int tableSizeFor(int cap) { |
1 | 此方法使用 移位 和 或运算 来实现的 |
5. 插入
put(K key, V value)
我们发现put
方法很简单,其核心方法是hash()
和putVal()
。
1 | public V put(K key, V value) { |
hash(Object key)
该方法是返回key
的hash
值。主要是通过将key
的hashcode
的高16位和低16位进行异或处理,这样做的目的是,使得高位也参加hash
的运算,使得哈希尽可能的均匀分布。
1 | static final int hash(Object key) { |
putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
hash
:当前key
的hash
值;
onlyIfAbsent
:为true
时不改变原有的值,为false
用value
替代旧值;
evict
:在HashMap
的子类LinkedHashMap
中会有用。
数组的初始化在该方法中。
使用hash & (length -1)
来计算index
,相当于取模运算,但是速度更快。
这是数组长度为2的幂的第一个用处,方便使用位运算来求
index
该方法,若该节点不存在,则将该节点插入,返回为null
;否则,旧值替换新值,返回旧值。
1 | final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { |
6. 扩容
扩容的条件:size > threshold
,而初始值是在构造函数中通过this.threshold = tableSizeFor(initialCapacity);
得到的。
扩容有两种情况:
a) 进行数组的初始化,此时oldCap = 0
。还能根据构造函数的不同分为两种情况:
1) 使用带参的构造函数,有threshold
,通过tableSizeFor(initialCapacity)
得到。
2) 使用不带参的构造函数,没有threshold
,通过默认的容量大小*默认负载因子得到。
当数组初始化完成后直接返回。
b) 数组被初始化过,数组被扩容为原来的两倍,此时oldCap > 0
。数组扩容后,需要将原数组的元素移到新的数组上。
1 | final Node<K,V>[] resize() { |
扩容会伴随一次重新的hash
分配,并且会遍历一遍原来哈希表中的所有元素,消耗大量资源,所以我们应该避免resize
.
7. 获取
获取元素要比插入元素简单,我们知道哈希表的最大特点就是不需要遍历可以直接找到元素所在的“桶位”。
当我们找到元素的桶位时,也分3中情况:只有一个节点(或者第一个节点就是);是树节点;是链表。
get(Object key)
1 | /** |
getNode(int hash, Object key)
1 | /** |
containsKey(Object key)
1 | /** |
containsValue(Object value)
1 | /** |
8. 删除
删除有两种方式:
remove(Object key)
根据key
删除remove(Object key, Object value)
根据key
和value
删除
需要注意:只有扩容操作,没有减少容量的操作。
remove(Object key, Object value)
1 | /** |
remove(Object key)
1 | /** |
removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)
1 | /** |