Map 是一种键-值对(key-value)集合,Map 集合中的每一个元素都包含一个键对象和一个值对象。其中,键对象不允许重复,而值对象可以重复。并且值对象还可以是 Map 类型的,就像数组中的元素还可以是数组一样。但是Map不保证映射元素的顺序HashMap只允许存在一个为空的键,但可以存在多个为空的值
先来看一下源码中几个比较重要的成员变量
//初始化容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量上限
static final int MAXIMUM_CAPACITY = 1 << 30;
//加载因子默认值
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当链表长度大于TREEIFY_THRESHOLD时,则转换为红黑树结构
//可以理解为链表的最大长度
static final int TREEIFY_THRESHOLD = 8;
//当红黑树长度小于UNTREEIFY_THRESHOLD时,转换为链表长度
//可以理解为红黑树的最小长度
static final int UNTREEIFY_THRESHOLD = 6;
-----------------------------------------------------------------------------------------------
//实际存储key和value的数组,这里将key和value封装成了Node对象。
transient Node<K,V>[] table;
//键值对(key-value)的总个数
transient int size;
//修改次数
transient int modCount;
//扩容的时机
//threshold表示当HashMap的size大于threshold时会执行resize进行扩容操作。
//threshold=capacity*loadFactor
int threshold;
//加载因子的实际值
//加载因子用来衡量HashMap满的程度。loadFactor的默认值为0.75f。计算HashMap的实时加载因子的方法为:size/capacity,而不是占用桶的数量去除以capacity。
final float loadFactor;
构造函数
无参构造函数
//无参构造函数
public HashMap() {
//赋值默认的加载因子 0.75F
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
参数为int类型
//当指定初始容量时,会使用初始容量和构造因子,调用另一个构造函数构造一个空的hashMap
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//这个构造函数就是上边的this
//initialCapacity 是指定的初始化容量
//loadFactor 是指默认的加载因子
public HashMap(int initialCapacity, float loadFactor) {
//判断初始化容量是否小于0
if (initialCapacity < 0)
//小于0则抛出异常
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//判断初始化容量是否大于最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
//是 则将容器初始化为最大容量
initialCapacity = MAXIMUM_CAPACITY;
//判断,当加载因子小于0或者加载因子为空时抛出异常。
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//通过tableSizeFor方法计算出不小于指定容量的最小的2的幂的结果,赋值给成员变量threshold
this.threshold = tableSizeFor(initialCapacity);
}
参数为Map集合
public HashMap(Map<? extends K, ? extends V> m) {
//默认加载因子0.75f
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
//获取参数集合大小
int s = m.size();
//判断参数集合大小是否 > 0
if (s > 0) {
//判断当前table数组是否为空(是否初始化)
if (table == null) { // pre-size
//根据参数集合大小计算出要创建的hashMap容量
//计算方法:参数集合大小 / 加载因子(0.75) + 1
//若计算结果大于上限容量 则设置新集合的容量为上线容量,反之设置计算结果为新集合容量
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
//若已经初始化,则判断参数集合大小,若大于threshold(扩容阈值),则先调用resize()方法进行扩容。
else if (s > threshold)
//扩容
resize();
//将key-value取出,利用key算出hash值再进行存入本地HashMap实例
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
//计算hash值
static final int hash(Object key) {
int h;
//首先判断key是否为空。(key == null)
//若为空 则为 0
//若不为空 则首先通过hashCode()方法计算key的hashCode值,然后赋值给h,最后带符号右移16位
//int 32位 1111 1111 1111 1111 0000 0000 0000 0000
// ^ 此符号表示异或 对象的hashCode的值的高位(前16位)
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//异或运算的作用:降低hashCode的冲突率。使hashCode的值更加随机。
在了解方法之前,先看一下hashMap的数据结构图
put方法
public V put(K key, V value) {
//根据key值获取hashCode值,调用putVal方法新增
return putVal(hash(key), key, value, false, true);
}
//putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//首先判断当前实例化数组table是否为空或者长度是否为0
if ((tab = table) == null || (n = tab.length) == 0)
//初始化table数组获取长度。
n = (tab = resize()).length;
//首先将table数组长度-1,然后和传进来的key的hash值做"与(&)"运算,最后赋值给i
//而变量i就是元素在数组中存储的位置
//在将tab中下标为i的元素节点取出赋值给p
//最后判断这个链表对象p是否为空
if ((p = tab[i = (n - 1) & hash]) == null)
//如果为空,则创建节点存放元素
tab[i] = newNode(hash, key, value, null);
//如果p 不为空
else {
Node<K,V> e; K k;
//判断p的hash和key与传进来的hash和key是否相同
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//取出p节点的元素赋值给Node<K,V> e;
e = p;
//判断当前p的数据结构是不是树结构
//jdk1.8的优化方案,推出了红黑树结构,提高性能。
else if (p instanceof TreeNode)
//基于红黑树的插入逻辑进行处理
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//如果不是红黑树结构,则以链表形式插入
else {
for (int binCount = 0; ; ++binCount) {
//判断p的下一个元素是否为空
if ((e = p.next) == null) {
//为空则创建元素节点,存入数据
p.next = newNode(hash, key, value, null);
//判断当前链表的数量是否大于树结构的预值(8)
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;
//将e节点对象赋值给与运算出的p元素节点
p = e;
}
}
//判断如果e不等于空
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//修改次数+1
++modCount;
//判断当前数组大小是否大于预值
if (++size > threshold)
//大于预值,则扩容
resize();
afterNodeInsertion(evict);
return null;
}
get方法
相对于put方法而言,get方法相对比较简单
public V get(Object key) {
Node<K,V> e;
//使用key获取对应的hash值 hash(key)
//获取对应的元素节点 getNode(hash(key), key)
//三目运算符 e == null ? null : e.value;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
get方法首先通过key值获取对应的hash值,再利用getNode方法获取对应元素节点,若为空则返回null,不为空则返回节点中对应的value值。
tableSizeFor
/**
* Returns a power of two size for the given target capacity.
*/
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;
}
tableSizeFor方法的作用就是返回大于等于cap的最小的2的倍数。(如果cap是2的倍数,那么返回值就是cap)
方法分析
首先,为什么要对cap做减1操作。int n = cap - 1;
这是为了防止,cap已经是2的幂。如果cap已经是2的幂, 又没有执行这个减1操作,则执行完后面的几条无符号右移操作之后,返回的capacity将是这个cap的2倍。
如果n这时为0了(经过了cap-1之后),则经过后面的几次无符号右移依然是0,最后返回的capacity是1(最后有个n+1的操作)。
这里只讨论n不等于0的情况。
由于n不等于0 ,所以转化为二进制后肯定有一个bit上的数是1,这是考虑最高位的1,通过无符号右移1位后再做或运算,(或运算:有一个1则为1,否则为0),那么这是就是000011xxxxxxx ,如下图:
这时进入第二次位移,将或运算
后的结果再右移2,假设此时n等于0000 1101 那么位移2位后是 00000011 ,再做或运算后就是 0000 1111。
以此类推,需要注意的是,**容量最大也就是32bit的正数,因此最后n |= n >>> 16;
,最多也就32个1,但是这时已经大于了MAXIMUM_CAPACITY
,所以取值到MAXIMUM_CAPACITY
。 **
Hash方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
三目运算符比较简单就不讲了
主要看这一部分(h = key.hashCode()) ^ (h >>> 16);
从上面的代码可以看到key的hash值的计算方法。key的hash值高16位不变,低16位与高16位异或^
作为key的最终hash值。(h >>> 16,表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位不变。) 如下图:
为什么要这么干呢?
这个与HashMap中table下标的计算有关。
n = table.length;
index = (n-1) & hash;
因为,table的长度都是2的幂,因此index仅与hash值的低n位有关,hash值的高位都被与操作置为0了。
假设table.length=2^4=16。
由上图可以看到,只有hash值的低4位参与了运算。
这样做很容易产生碰撞。设计者权衡了速度、效用和质量,将高16位与低16位异或来减少这种影响。设计者考虑到现在的hashCode分布的已经很不错了,而且当发生较大碰撞时也用树形存储降低了冲突。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。
内部类Node
内部类Node是HashMap中不可或缺的一份子。我们知道HashMap源码结构是以数组+链表+红黑树组成。而Node就是HashMap中的链表对象类。
//内部类Node
static class Node<K,V> implements Map.Entry<K,V> {
//hash值
final int hash;
//key
final K key;
//value
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;
}
//获取key
public final K getKey() { return key; }
//获取value
public final V getValue() { return value; }
//toString
public final String toString() { return key + "=" + value; }
//hashCode
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//setValue
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//equals
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;
}
}
扩容机制
final Node<K,V>[] resize() {
//获取当前数组(旧数组)
Node<K,V>[] oldTab = table;
//获取旧数组长度,若数组为空长度为0。
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//旧数组的扩容阈值(扩容时机)
int oldThr = threshold;
//扩容后的变量(新数组长度,扩容阈值)
int newCap, newThr = 0;
//如果旧数组长度大于0
if (oldCap > 0) {
//如果旧数组长度大于等于最大容量上限
if (oldCap >= MAXIMUM_CAPACITY) {
//则设置扩容阈值(扩容时机)为int类型的最大值
threshold = Integer.MAX_VALUE;
//返回
return oldTab;
}
//如果不等于最大容量上限
//1、将旧的容量左移1位(旧的容量*2) 赋值给新的容量
//2、判断新数组的容量是否小于容量上限值(MAXIMUM_CAPACITY)并且大于等于容量初始值(DEFAULT_INITIAL_CAPACITY)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//将旧的扩容阈值左移1位(*2)赋值给新的扩容阈值
newThr = oldThr << 1; // double threshold
}
//如果旧数组长度小于或者等于0
//判断旧的扩容阈值是否大于0
else if (oldThr > 0) // initial capacity was placed in threshold
//如果旧的扩容阈值大于0
//将旧的扩容阈值赋值给新的扩容阈值变量
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//如过旧的扩容阈值小于等于0
//将新的容量设置为初始化容量 -- 注意这里是容量不是扩容阈值
newCap = DEFAULT_INITIAL_CAPACITY;
//计算出新的扩容阈值赋值给newThr
//计算方法:初始化容量*加载因子
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果(计算后的)扩容阈值等于0
if (newThr == 0) {
//将新的容量*旧数组的加载因子的值赋值给ft
float ft = (float)newCap * loadFactor;
//1、判断容量小于容量上限值 并且 判断计算后的ft小于容量上限值
//若同时为true 设置新的扩容阈值为计算后的ft
//否则 设置新的扩容阈值为int类型的最大值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//将当前的扩容阈值设置为新的扩容阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//new一个新的数组,长度为新的容量值(newCap)
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//设置当前数组为新的数组
table = newTab;
//如果旧数组不为空
if (oldTab != null) {
//for循环
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//循环判断下标为j的位置是否"不为空"
if ((e = oldTab[j]) != null) {
//清空当前位置的元素
oldTab[j] = null;
//判断当前位置的下一个元素节点是否为空
if (e.next == null)
//重新计算位置,进行元素保存
//新元素的位置 : 使用e.hash & 新元素扩容阈值-1
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;
//根据e.hash & oldCap == 0 的规则划分为两个不同的链表
//二进制最高位 == 0 这是索引不变的链表元素
if ((e.hash & oldCap) == 0) {
//如果旧元素的尾部 "为空"
if (loTail == null)
//设置旧元素的头部为当前元素节点
loHead = e;
//如果旧元素的尾部 "不为空"
else
//设置旧元素的下一个元素为当前元素节点
loTail.next = e;
//设置e为旧元素
loTail = e;
}
//二进制最高位 == 1 这是索引发生改变的链表元素
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//将索引不变的链表存入桶中(newTab)
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead; //存入j的位置
}
//将索引改变的链表存入桶中(newTab)
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead; //存入j+oldCap的位置
}
}
}
}
}
return newTab;
}
经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
元素在重新计算hash之后,因为n是原来的两倍,那么n-1的范围在高位多出1bit(红色)。因此新的下标就会发生这样的变化:
因此,在扩展HashMap的时候,只需要判断新增的bit是1还是0就可以。若是0则原索引地址不变,若是1 则是原索引地址+旧扩展阈值(oldCap) 。下面是16扩充为32的示意图。
文章参考:
Java 8系列之重新认识HashMap
HashMap方法hash()、tableSizeFor()