最近在学习java集合之一的hashMap,为了加深记忆,记录一下笔记。
一、什么是哈希表
哈希表又叫散列表,是根据关键码值(Key value)而直接进行访问的数据结构。在哈希表中进行添加、删除、查找等操作,性能十分高,在不考虑哈希碰撞的情况下,时间复杂度为O(1)。
二、HashMap
HashMap是用哈希表+链表+红黑树实现的map类。它继承了AbstractMap,而AbstractMap实现了Map接口
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
这里比较有趣的地方是,HashMap的父类AbstractMap已经实现了Map接口,但是源码中HashMap类又实现了一次Map接口,这一步其实是多余的,集合的源码作者也承认了这里写错了,但是大佬就是人性,不改!
HashMap并非线程安全,如果同时存在多个线程写入数据,可能会造成数据不一致。
HashMap的数组部分被称作哈希桶,当链表数据长度超过8时候,会以红黑树形式存储,当长度降低到6的时候,转换会链表形式
链表时间复杂度O(n) 红黑树时间复杂度O(log n)
(图片来源:www.jianshu.com/p/fb282d3d2…
简单使用HashMap
HashMap<Integer, String> hm = new HashMap<>();
System.out.println(hm.put(1, "小白"));
System.out.println(hm.put(2, "小红"));
System.out.println(hm.put(3, "小蓝"));
System.out.println(hm.put(1, "小绿"));
System.out.println(hm.put(4, "小黑"));
System.out.println("hashmap = " + hm);
System.out.println("hashmap 大小:" + hm.size());
输出结果:
null
null
null
小白
null
hashmap = {1=小绿, 2=小红, 3=小蓝, 4=小黑}
hashmap 大小:4
通过代码发现,hm的size是4,而不是5,hm.put(1,"小绿")返回了小白。为什么呢? 带着疑问查看下源码:
·HaspMap类中比较重要的属性
//默认的初始化数组容量 1<<4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大的数组容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//装载因子,代表了数组的填充度有多少,默认是0.75
//至于为什么是0.75,源码给出的解析是
//(As a general rule, the default load factor (.75) offers a good
//tradeoff between time and space costs. Higher values decrease the
//space overhead but increase the lookup cost )
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//用来接受用户传进来的装载因子参数,默认情况就是DEFAULT_LOAD_FACTOR
final float loadFactor;
//底层主数组
transient Node<K,V>[] table;
//记录数组中添加的元素数量
transient int size;
//阈值,当size超过这个值后,数组会扩容
int threshold;
//用来记录HashMap内部结构发生变化的次数
transient int modCount;
//超过8时候,会转成用红黑树存储
static final int TREEIFY_THRESHOLD = 8;
·算出hash值的方法
//向右移了16,将高位降低下来,降低了函数的离散度,减少出现哈希碰撞
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
·Map.put方法实现
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//初始化table或扩容
n = (tab = resize()).length;
//i = (n - 1) & hash 算出哈希桶中的下标
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> 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<K,V>)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);
//如果bin数量超过8,则采用红黑树形式存储
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//这里如果已经存在的情况,则直接把新的val赋值进去,返回旧的val,揭秘了hm.put(1,"小绿")为什么返回了小白
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//数组内的元素大于阈值,进行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
//put成功后返回null,System.out.println(hm.put(2, "小红"))为什么返回null的原因
return null;
}
关于计算下标:i = (n - 1) & hash
我上面的代码使用的Key是Integer,假如key=1的时候,hash值是1, 运算后下标就是1,假如key=2,则下标是2
key=1 0000 0001
n-1=15 & 0000 1111
0000 0001
key=2 0000 0010
n-1=15 & 0000 1111
0000 0010
·初始化table或扩容的方法
//resize主要用于两种情况
//1.初始化table
//2.在table大小超过threshold之后进行扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//超过最大容量时,不再扩容,直接返回
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//扩容,oldCap << 1 变成原来的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//向左移一位,增加两倍,每次扩容都是2的N次幂
newThr = oldThr << 1;
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else {
//新初始化的hashMap,默认的大小是16
newCap = DEFAULT_INITIAL_CAPACITY;
//默认的阈值是16*0.75 = 12
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<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//扩容情况下,把原来table的数据搬到扩容后的新table
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果链中没有数据,则直接存到新表中
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果是红黑树,则拆分树
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
//这里定义两个链表
//一个用来记录低位的数据,一个用来记录高位数据
//分别用loHead和loTail指向它的头节点和尾节点
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
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);
//如果lo链表不为空,则将整个链表数据放到新table上
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//如果hi链表不为空,则将整个链表数据放到新table上
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
e.hash & oldCap == 0, 证明元素在新表的下标与旧表的下标一致,都是j,
e.hash & oldCap == 1,证明元素在新表的下标是j+oldCap
最后一张图,是我学习理解画的结构图
总结
1.hashMap初始默认长度是16,扩容阈值是16*0.75=12,当size的值超过了阈值,表就会进行扩容
2.创建表会发生在第一次调用put方法时候,如果table不存在则创建
3.hashMap每次扩容都是2的次幂
4.扩容后旧表的数据会重新装载到新表,下标也会有一定关系
5.添加元素时候会,如果是原来的已经存在的key,则替换val,返回旧的val,如果发生哈希冲突,则往链表末端追加,当binCount超过8时候,则改变成红黑树形式存储
最后,观看源码使我学习到了很多,加深了对数据结构的认识,对位运算的使用有了深刻的认知,加油!