0.底层结构
首先看HashMap的底层结构和基本方法
transient Node<K,V>[] table; //数组,hash表的桶
//hash表每个桶下的链表
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
还要注意的是,HashMap不是一个线程安全的集合!!!
都知道Map有一个很标志的特点,存储键值对。
就是这个接口!Entry!
//An object that maps keys to values.
interface Entry<K,V>{
K getKey();
V getValue();
}
-
Map集合迭代器输出
//1.Map->Set Set<Map.Entry<Integer, String>> key = map.entrySet(); //2.取得Set迭代器 Iterator<Map.Entry<Integer, String>> iterator = key.iterator(); //3.迭代输出 while (iterator.hasNext()){ Map.Entry<Integer,String> entry = iterator.next(); System.out.println(entry.getKey()+"="+entry.getValue()); }
了解一下HashMap的各种参数~,马上开始看源码
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//初始化容量为16(桶的数量)
static final int MAXIMUM_CAPACITY = 1 << 30;
//最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//负载因子:0.75
static final int TREEIFY_THRESHOLD = 8;
//树化门限值:8
static final int UNTREEIFY_THRESHOLD = 6;
//解树化,返回链表的阈值:6
static final int MIN_TREEIFY_CAPACITY = 64;
//树化的最少元素个数:64
1.创建HashMap
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
//DEFAULT_LOAD_FACTOR->负载因子,默认0.75
}
transient int modCount; //提一嘴这个属性,记录HashMap更改次数
很明显HashMap的创建并没有大张旗鼓地开始初始化Hash表,仅仅只是初始化了负载因子?(无参构造)
那什么时候开始真正地创建呢?
2.添加元素
public V put(K key,V value) {
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;
if ((tab = table) == null || (n = tab.length) == 0)
//调用resize方法初始化
n = (tab = resize()).length;
//判断hash桶中是不是装有结点
if ((p = tab[i = (n - 1) & hash]) == null)
//没有结点,将key和value值封装成一个node结点放入对应的桶中
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//当put的key值是已有值时
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);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
//若该key值已经存在
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//若key值重复,更新value值
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(),最后好像扩容也用到了?
是的,resize()负责hash表的初始化和扩容。
3.扩容
主要代码如下:扩容2倍
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) {
//旧桶大小已经大于1<<30大小了!
threshold = Integer.MAX_VALUE;
return oldTab;
}
//容量*2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//旧桶还可以扩容,阈值扩大2倍!
newThr = oldThr << 1; // double threshold
}
//-----------------------初始化功能---------------------
//旧桶为空,但是旧的阈值大于0(有参构造)
else if (oldThr > 0) // initial capacity was placed in threshold
//新容量就等于旧的阈值
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//否则新容量、阈值为默认大小
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//新阈值是0(有参构造传入的容量*0.75太小,为0)
//重新设置
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//最后更新阈值
threshold = newThr;
}
}
4.树化
主要看这段~,最开始说了树化阈值是8,又说了树化的最大元素是64.
只有同时满足才会树化一个桶!
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//hash表为空||表内全部元素小于阈值(默认64)
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//扩容
resize();
5.一个元素怎么存到Hash表中?
-
hashCode()这是一个native本地方法,不需要了解
-
hash(Object)
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); -
putVal()中
p = tab[i = (n - 1) & hash]
- 也就是先通过native方法计算这个对象的hash值,每个对象有一个hash值。我们可以看到hashCode()的返回值是int类型,int大概是2^31左右。肯定不能直接放进数组!
- hash()进行处理,要是为key为null,结果为0,否则把原来的hash值和hash右移16位后的值做^运算,返回这个值。
- 在putVal中,将上一步处理好的hash值和hash表的长度-1做&运算,得到最后的下标。
至于为什么,下面讲解!
6.许多问题的解答~
问题1:为什么树化?
因为可以大幅度缩减查找元素的时间,红黑树的查找比链表的查找快了O(n)/O(log2 n)倍。
问题2:怎么树化?
如果容量小于树化阈值64,就只会简单的扩容,若大于并且桶中元素大于8个,则树化。
HashMap和HashSet初始化方式是一样的,都是懒加载策略,也就是一开始构造里面不做真正的初始化,在第一次添加元素的时候才真正初始化。
resize()方法有两个作用:1.初始化数组(桶)2.扩容
问题3:扩容多少?
初始化一般是默认容量16.
扩容:newCap = oldCap<<1;每次都是*2
问题4:负载因子和容量?
负载因子就相当于可用余额占全部空间的百分比,一般是0.75
容量就是真正的总空间大小,有一点防患于未然的意思。
阈值(可用余额) = 负载因子*容量
初始阈值 = 负载因子(默认0.75)*默认容量(也就是初始化容量,16) = 12
newThr = oldThr <<1;每次扩容2倍
这个阈值就是桶的最大可用空间大小,一旦超过,就会扩容,但其实还有0.25的空间,就是提前预防。
问题5:那hashcode怎么和桶的i联系起来呢?
上文已经说明,hashMap通过两次处理得到i值,起作用很明显,就是避免哈希碰撞!
第一次把原始的hash值和右移16位的自己异或运算,作用打乱了hash的顺序,避免碰撞。
第二次再和n-1做与运算,这一步作用就是规范hash大小(规范到n以内)。
问题6:为什么hash表的length最好是2^n?
2^n-1就相当于后面的低位都变成了1,那么&操作的结果最大程度上取决于hash地址,唯一性提高,而且位运算效率更高。