HashMap是散列表,它是基于高速存取的角度设计的,也是一种典型的“空间换时间”的做法。顾名思义,散列表能够理解为一个线性表,可是当中的元素不是紧密排列的,而是可能存在空隙。散列表(Hash table,也叫哈希表),是依据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值(key)映射到表中一个位置来访问记录(value),以加快查找的速度。这个映射函数叫做散列函数(在HashMap中就是hash算法),存放记录的数组叫做散列表。
为了达到高速存取的目的,我们通常存储7个元素的时候通常会申请10个空间,7/10=0.7,这个数字叫做负载因子。负载因子越小,发送hash膨胀(通过hash算法得到桶数组位置重复)的概率就越小。负载因子也不是越小越好,越小代表浪费了很多的空间。
发生hash碰撞解决冲突是个复杂问题,冲突主要取决于:
(1)散列函数,一个好的散列函数的值应尽可能平均分布。
(2)处理冲突方法。
(3)负载因子的大小。
解决冲突的办法:
(1)线性探查法:冲突后,线性向前试探,找到近期的一个空位置。缺点是会出现堆积现象。存取时,可能不是同义词的词也位于探查序列,影响效率。
(2)双散列函数法:在位置d冲突后,再次使用还有一个散列函数产生一个与散列表桶容量m互质的数c,依次试探(d+n*c)%m,使探查序列跳跃式分布。
经常使用的构造散列函数的方法
散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位:
1. 直接寻址法:取key或key的某个线性函数值为散列地址。即H(key)=key或H(key) = a•key + b,当中a和b为常数(这样的散列函数叫做自身函数)
2. 数字分析法:分析一组数据,比方一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体同样,这样的话,出现冲突的几率就会非常大,可是我们发现年月日的后几位表示月份和详细日期的数字区别非常大,假设用后面的数字来构成散列地址,则冲突的几率会明显减少。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
3. 平方取中法:取key平方后的中间几位作为散列地址。
4. 折叠法:将key切割成位数同样的几部分,最后一部分位数能够不同,然后取这几部分的叠加和(去除进位)作为散列地址。
5. 随机数法:选择一随机函数,取key的随机值作为散列地址,通经常使用于key长度不同的场合。
6. 除留余数法:取key被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p, p<=m。不仅能够对key直接取模,也可在折叠、平方取中等运算之后取模。对p的选择非常重要,一般取素数或m,若p选的不好,容易产生同义词。
Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也可以使用红黑树进行存储,下面看jdk1.8如何实现HashMap的。
上图很形象的展示了HashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表,也可能是红黑树,红黑树的引入是为了提高效率。
public class HashMap extends AbstractMap
implements Map, Cloneable, Serializable
HashMap 实现了Map,Cloneable,Serializable接口继承了AbstractMap接口
AbstractMap此类提供 Map 接口的骨干实现,以最大限度地减少实现此接口所需的工作。要实现不可修改的映射,编程人员只需扩展此类并提供 entrySet 方法的实现即可,该方法将返回映射的映射关系 set 视图。通常,返回的 set 将依次在 AbstractSet 上实现。此 set 不支持 add 或 remove 方法,其迭代器也不支持 remove 方法。
要实现可修改的映射,编程人员必须另外重写此类的 put 方法(否则将抛出 UnsupportedOperationException),entrySet().iterator() 返回的迭代器也必须另外实现其 remove 方法。
// 序列号
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量,其值必须是2的倍数
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的装载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当桶(bucket)上的结点数大于这个值8时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当桶(bucket)上的结点数小于这个值6时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
// 桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
// 存储元素的数组,它的length总是2的幂次倍
transient Node[] table;
// 存放具体元素的集
transient Set> entrySet;
// entrySet的个数也就是key-value的个数
transient int size;
// 每次扩容和更改map结构的计数器
transient int modCount;
// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
int threshold;
//装载因子
final float loadFactor;
HashMap有四个构造函数
构造函数 | 作用 |
public HashMap() | 构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。 |
public HashMap(int initialCapacity) | 构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap。 |
public HashMap(int initialCapacity, float loadFactor) | 构造一个带指定初始容量和加载因子的空 HashMap。 |
public HashMap(Map extends K, ? extends V> m) | 构造一个映射关系与指定 Map 相同的新 HashMap。所创建的 HashMap 具有默认加载因子 (0.75) 和足以容纳指定 Map 中映射关系的初始容量。 |
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(int initialCapacity) {
//掉用public HashMap(int initialCapacity, float loadFactor)构造函数
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
//当容量小于0抛异常
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;
// 初始化threshold大小(其值为大于initialCapacity的最小的二次幂数值。)
this.threshold = tableSizeFor(initialCapacity);
}
上述初始化过程用到tableSizeFor(int cap)函数,作用是求大于cap的最小的二次幂数值
static final int tableSizeFor(int cap) {
int n = cap - 1;//这里是因为考虑到cap为2的整数次幂的情况
//1. 假设此时n的二进制最高位1在第i位(最低位为第0位)
n |= n >>> 1;
//2. 此时n的二进制第i, i-1位都为1
n |= n >>> 2;
//3. 此时n的二进制第i, i-1, i-2, i-3位都为1
n |= n >>> 4;
//4. 此时n的二进制第i, i-1, i-2, i-3, i-4, i-5, i-6, i-7位都为1(当然,严谨点应该再假设i>7)
n |= n >>> 8;
//5.---------
n |= n >>> 16;
//6.---------
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
2的幂次值的数的二进制都是这样的
十进制 | 二进制 |
2 | 000010 |
4 | 000100 |
8 | 001000 |
16 | 010000 |
32 | 100000 |
int n = cap - 1;
如果已经是2的幂次值,减1能避免所求的数错误
上述
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
int有效位是31位,上述能把最高位为1后面的低位全变成1,最后再+1,进位为2的幂次值这样的数
public HashMap(Map extends K, ? extends V> m) {
//负载因子赋值
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
上述构造函数主要依赖于putMapEntries把键值对放到map中
final void putMapEntries(Map extends K, ? extends V> m, boolean evict) {
//求m的键值对个数
int s = m.size();
//m不为空
if (s > 0) {
//如果桶数组为null
if (table == null) { // pre-size
//求出装下s所需的空间
float ft = ((float)s / loadFactor) + 1.0F;
//ft不能大于最大容量
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
//t大于当前阈值
if (t > threshold)
//重新计算阈值
threshold = tableSizeFor(t);
}
//s大于阈值进行扩容处理
else if (s > threshold)
resize();
//遍历map,把值放进去
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);
}
}
}
put时计算桶数组的位置用到
i = (n - 1) & hash
一般情况下取桶数组的位置一般是key的hash关于桶数组的length取余
hash % n == (n-1) & hash
当n是2的指数时,等式成立。但是,对于现代的处理器来说,除法和求余数(模运算)是最慢的,HashMap设计的就是这么巧妙
上述构造函数主要依赖于
resize();
putVal(hash(key), key, value, false, evict);
函数,上述函数在下面进行分析
static class Node implements Map.Entry {
final int hash;存储元素key的hash值
final K key;
V value;
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
HashMap 添加元素的方法是put,而put方法有三个
public V put(K key, V value) | 向此map中添加一个映射关系,如果存在重复的key,那么会把新的value替换掉原来的value值。 |
public void putAll(Map extends K, ? extends V> m) | 添加所有 |
public V putIfAbsent(K key, V value) | 向此map中添加一个映射关系时,如果存在重复的key,那么putIfAbsent不会放入值。 |
put方法的核心函数是 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict)
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
// table未初始化或者长度为0,进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {// 桶中已经存在元素
Node e; K k;
// 比较桶中第一个元素(数组中的结点)的hash值和key是否都相等
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//记录桶的第一个节点
e = p;
//key不等,且节点为树节点,调用putTreeVal
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
//key 不等,且节点为链表
else {
// 在链表最末插入结点
for (int binCount = 0; ; ++binCount) {
// 到达链表的尾部
if ((e = p.next) == null) {
//创建一个新节点放到链表尾部
p.next = newNode(hash, key, value, null);
// 节点数量达到转TREEIFY_THRESHOLD(8)阈值,转化为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 判断链表中结点的key值与插入的元素的key值是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表
p = e;
}
}
// 表示在桶中已经存在key的映射
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;
}
上面代码比较容易理解,其中涉及到的重要函数
resize()
putTreeVal(this, tab, hash, key, value);
treeifyBin(tab, hash);
其中putTreeVal、treeifyBin与红黑树有关稍后再说
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//旧的桶数组length大于0
if (oldCap > 0) {
//旧的桶数组length大于HashMap所容许的最大容量则不扩容直接返回
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//新桶数组length变为旧桶数组的2倍,新阈值变为原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//旧桶数组length为0,代表第一次初始化,切阈值大于0
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//旧桶数组length为0,,切阈值为0,初始化桶数组和阈值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
//下面开始把老数组中的元素存放到新数组中
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//遍历老数组
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//老桶数组j位置的节点只有1个元素,重新hash计算该节点位于新桶数组的位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
// //新表是旧表的两倍容量,实例上就把单链表拆分为两队,
//e.hash&oldCap为偶数一队,e.hash&oldCap为奇数一对
// (e.hash & oldCap) 得到的是 元素的在数组中的位置是否需要移动,示例如下
// 示例1:
// e.hash=10 0000 1010
// oldCap=16 0001 0000
// & =0 0000 0000 比较高位的第一位 0
//结论:元素位置在扩容后数组中的位置没有发生改变
// 示例2:
// e.hash=17 0001 0001
// oldCap=16 0001 0000
// & =1 0001 0000 比较高位的第一位 1
//结论:元素位置在扩容后数组中的位置发生了改变,新的下标位置是原下标位置+原数组长度
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;
}
重点介绍
假如现在容量为初始容量16,再假如5,21,37,53的hash自己(二进制),
所以在oldTab中的存储位置就都是 hash & (16 - 1)【16-1就是二进制1111,就是取最后四位】,
5 :00000101
21:00010101
37:00100101
53:00110101
四个数与(16-1)相与后都是0101
即原始链为:5--->21--->37--->53---->null
此时进入代码中 do-while 循环,对链表节点进行遍历,判断是留下还是去新的链表:
lo就是扩容后仍然在原地的元素链表
hi就是扩容后下标为 原位置+原数组容量 的元素链表,从而不需要重新计算hash。
因为扩容后计算存储位置就是 hash & (32 - 1)【取后5位】,但是并不需要再计算一次位置,
此处只需要判断左边新增的那一位(右数第5位)是否为1即可判断此节点是留在原地lo还是移动去高位hi:(e.hash & oldCap) == 0 (oldCap是16也就是10000,相与即取新的那一位)
5 :00000101——————》0留在原地 lo链表
21:00010101——————》1移向高位 hi链表
37:00100101——————》0留在原地 lo链表
53:00110101——————》1移向高位 hi链表
为什么为0就放在原位置,为1就要放到原位置+原数组容量位置呢
因为上面进行resize的时候,是将数组容量扩大了一倍,原计算位置取模的时候是通过length-1,那么现在与oldCap相与后,如果右边第5位是1,那就是增加了一个原数组的长度(因为原取模的时候是取4位),所以如果右边第5位要是1的话,那新的位置就是原位置+原数组容量。