HashMap基础
HashMap的成员变量
静态变量
1.哈希桶数组的默认长度为16,同时其长度一定要为2^n,默认负载系数为0.75,其最大长度为2的30次方。
2.链表树化的条件是:哈希桶数组的长度大于等于64且链表中节点的个数大于等于8。
3.红黑树链表化的条件是:树中节点数小于等于6。
//哈希桶数组的默认长度(16)二进制:10000。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//哈希桶数组的最大长度(2^30)
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的负载系数
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树化的最小链表节点数(链表节点数要大于等于8且哈希桶数组的长度大于等于64)
static final int TREEIFY_THRESHOLD = 8;
//由树转为链表的节点数,当树中节点数小于该值会转换为链表。
static final int UNTREEIFY_THRESHOLD = 6;
//树化的最小哈希桶数组值
static final int MIN_TREEIFY_CAPACITY = 64;
实例变量
1.哈希桶数组存放的数据类型可以是链式节点(Node),也可以是树式节点(TreeNode继承自Node)。
2.threshold是桶扩充的阈值,这个阈值等于capacity * loadfactor,当键值对的数量超过这个阈值时会扩容。
//哈希桶数组
transient Node<K,V>[] table;
//存储键值对的Set,存储的类为Map中的内部类
transient Set<Map.Entry<K,V>> entrySet;
//键值对的数量
transient int size;
//HashMap结构修改的次数
transient int modCount;
//扩容的阀值,当键值对的数量超过这个阀值会产生扩容
int threshold;
//负载因子
final float loadFactor;
链式节点
这是链式节点的存储结构,其实是实现了内部接口Entry<K,V>。树式节点继承自链式节点,这里不写出来了。
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的构造函数
无参数构造函数:构造一个默认容量(16)与默认负载系数(0.75)的HashMap。
/**
* 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
}
传入容量的构造函数,与无参构造函数类似,会将传入的容量转化为大于该值的最小的2的幂次方,并赋值给扩充阈值。
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
传入容量和负载系数的构造函数,这里的tableSizeFor方法可以理解为将传入的容量转化为大于该值的最小的2的幂次方,比如传入6,就会返回8。可以看到HashMap没有在构造函数中初始化hash桶数组。
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);
}
HashMap减少哈希冲突与解决哈希冲突的方法
哈希冲突
比如现在有这几个key值:5,28,19,15,20,33,12,17,10,而我们的哈希桶数组大小是9,哈希函数为了简便起见为:
H(key)=key%9
会发现H(28)=1,而H(19)=1,这样就产生了哈希冲突。解决哈希冲突就是HashMap中要做的事情。
HashMap的hash函数与哈希桶数组下标的计算(重要)
HashMap是如何计算key的hash值呢,是通过hash函数。这里还可以知道HashMap存储的键值对允许key值为null。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
之后计算对应哈希桶数组的下标是通过这行代码,其中hash是通过上面的hash函数求出来的哈希值。
&是按位与
i=(n - 1) & hash
为什么要无符号右移16位后做异或运算(减少哈希冲突)
第一次看到这里我也是很奇怪为什么hash函数不直接使用key的hashCode,而是使用下面的这行代码。
>>>是无符号右移
^是异或
(h = key.hashCode()) ^ (h >>> 16);
可以理解为设计者想要将高低二进制特征混合,防止哈希桶数组长度较小时,哈希桶数组下标的计算结果只与哈希值的低16位有关,而造成哈希冲突。
以图中例子可知,h右移16位,相当于把高16位的数移动到了后11位,而在于h自身做异或操作后,原本高16位没有变化,低16位就变成了低16位与高16位的异或结果,这样可以将高低位二进制特征混合起来。
而之所以要在低16位的结果改为高低位混合的原因在于,哈希桶数组的长度往往不会很大,也就是说除非长度大于2^16+1,才会用到高16位。这样会造成的结果是:如果两个hash值在低16位没有差别,而差别在高16位,如果低16位结果没有改变的话,他们计算出的哈希桶数组下标就相同了,很容易出现哈希冲突。而HashMap这种设计方法就是为了减少哈希冲突。
计算下标的方法:
i=(n - 1) & hash
这就是一个n值为16的例子,可以看到高16位的二进制特征都丢失了。
为什么桶数组的长度是2^n(减少哈希冲突)
是为了让结果更加均匀。比如哈希桶数组的长度为17,那样17-1的结果就是16(00010000),可以看到,因为是与运算,最后计算出的下标值就只与hash值的第五位有关系了,其他值不管为0或1与运算之后都是0,这样会造成更大的哈希冲突。
而如果桶数组的长度为2^n,做完减1操作后,其二进制就有多个1(16-1=15(00001111)),相当于结果与hash值多个位置有关(所有二进制为1的位置),可以有效地减少哈希冲突。
为什么使用&而非%(节省时间)
其实理论上来说两种方式结果相同,不过按位与的操作会更快。因为%是算术运算,最终还是会转换为位运算、
HashMap解决哈希冲突的方法(重要)
HashMap的常规存储方式是数组,数组中存放 Node<K,V>的节点,但是为了防止出现哈希冲突,HashMap使用数组+链表+红黑树的存储方式。
HashMap理想的情况是不出现哈希冲突,一个桶中装一个值。但不幸的是,即使HashMap已经通过上述很多种方式减少哈希冲突,可是哈希冲突还是会出现。HashMap中使用链表和红黑树来解决哈希冲突。
HashMap会在链表的长度大于等于8且哈希桶数组的长度大于等于64时,会将链表树化。红黑树是一个自平衡的二叉查找树,因此查找效率就会从O(n)变成O(logn)。
static final int TREEIFY_THRESHOLD = 8;
static