目录
一、HashMap介绍
二、HashMap原理图
三、桶的个数必定为2的n次幂
四、HashMap中的put方法
五、链表转红黑树
HashMap采用
HashMap中存储数据是通过存入key的hash值进行存储的,我们先来看一下HashMap的构造方法.
//在HashMap的构造器中初始化桶的数量和装载因子
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中的部分域(属性).
//桶数量的默认值(16),而不是桶内可以装的元素的数量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//必须为2的n次方,所以最大为2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
/*
默认负载系数为0.75,当表中超过75%的位置已经填入元素,这个表就会用双倍的桶数自动地进行
再散列(rehashed),可以通过构造函数初始化
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//桶内数据量超过这个阈值就会将桶内数据结构从链表转为红黑树
static final int TREEIFY_THRESHOLD = 8;
//当小于这个数值时将红黑树转为链表
static final int UNTREEIFY_THRESHOLD = 6;
//在第一次使用时初始化Node数组,桶对象数组
transient Node[] table;
//该HashMap中共有多少个键值对
transient int size;
//桶的数量,默认为DEFAULT_INITIAL_CAPACITY,即16
int threshold;
从以上代码中我们可以看出,threshold属性就是桶的个数,而table就是这个桶的数组.我们可以暂时理解为第一次添加一个新元素的时候,会有一个table = new Node
那么这个Node
这其实涉及到内部静态类的概念.也就是Node定义在HashMap里,而不是和HashMap放在同一级别(.java文件中写两个类).以下是Node类型的定义:
/*
* 内部static类,对外部不可见,并且保证每个HashMap对象的内部类相同
*/
static class Node implements Map.Entry {
//存入元素的hash值
final int hash;
//存入元素的key值
final K key;
//key对应的value值
V value;
//桶内采用链表形式,指向下一个Node对象
Node next;
Node(int hash, K key, V value, Node 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;
}
}
在Node类里,我们发现有next属性,不用多说,这肯定是个链表.而在HashMap的table数组中,存储的只是第一个Node元素,也就是链表中的头结点.
好了以上HashMap的基本存储就说清楚了,接下来介绍一下桶的个数的相关问题.
桶的个数必定为2的n次幂,这是Java设计者定义的约定.在之前的提到的构造方法中有这样一段代码:
//如果输入的容量设定小于0,则抛出异常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果设定长度大于最大值,则设定为最大值,即2^30
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//装载因子小于等于0或者不是一个float类型的数,则抛出一个异常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
tableSizeFor方法:
//获得比cap要大且确保是2的幂次方的一个数
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
在该方法中,我们先不谈cap - 1,先来看下面的 n | n >>> 1.在Java中,我们知道>>>是右移高位补0,此时我们将原数与移位后得到的数进行或运算,此时我们就可以确保在最高位的后面那一位它的值也是1.下一次n >>> 2的时候就能够确保最高位后面跟上三个1...在所有的移位、或的操作后,我们得到的就是最高位后面全是1的一个int类型数据.此时n+1必定是一个2幂次的数.
详解如下:
eg:
先来分析有关n位操作部分:先来假设n的二进制为01xxx...xxx。接着
对n右移1位:001xx...xxx,再位或:011xx...xxx
对n右移2为:00011...xxx,再位或:01111...xxx
此时前面已经有四个1了,再右移4位且位或可得8个1
同理,有8个1,右移8位肯定会让后八位也为1。
综上可得,该算法让最高位的1后面的位全变为1。
最后再让结果n+1,即得到了2的整数次幂的值了。
我们先来说一下在HashMap中的加入机制吧,说完机制我们再来看代码.
首页呢,假设桶的数量为n,待插入的元素的key值的哈希值为hash.然后,我们通过求余操作计算 hash % n,假设结果为 t.此时我们就可以往 table[t] 中插入一条新的数据.也就是说通过hash值和桶的数量求余的操作得到要存入的链表的位置.
put方法实际上是调用的putVal方法,putVal代码如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode)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;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
在上面代码中,有下面这一段:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
i = (n - 1) & hash,这是将n - 1和hash进行二进制上的与操作,n在上一句通过 n = table.length赋值为桶的长度.比如说长度设为之前提到过的默认长度16,16-1=15,15 & hash实际上就是取余的操作.从第三章我们知道,一共有2^m个桶,所以代码中的n-1得到的是从第m位到第0位(从右至左)都是1,此时将hash与n - 1进行与操作得到的结果就跟hash % n得到的结果是一模一样的.这就是2的次幂的重要性.取余操作变成了位与,这大大优化了HashMap的性能.
当然了,你在putVal中采用了(n - 1) & hash的操作的话,桶的设计自然就不能是2的次幂以外的数了,比如说你设定桶的数量是9.hash值比方说传入了2和3,2 & 8 = 0,3 & 8 = 0.这样就冲突了.
我们已经清楚了HashMap的存储结构以及构造方法,不过在构造方法中还有一个变量没有提到过,那就是loadFactor.它是一个用来判断何时改变桶内存储结构的变量.
当桶内数据越来越多时,链表的查询效率显然就弱了,所以在超过一定存储数量之后,桶内的存储结构就从链表替换为了红黑树.这个加载因子有个默认值是0.75(一般来讲这个默认值足够了).
具体的可以看这篇文章,红黑树原理以及HashMap中链表到红黑树的转换和判断条件等,将的十分详细.
https://www.cnblogs.com/finite/p/8251587.html