你应该先阅读java集合系列一:前传
//默认初始容量 16
//为什么是16? 如果模数是2的幂,那么我们可以直接使用位运算来得到结果,效率要大大高于做除法
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//table的最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认加载因子 0.75
// 默认值0.75是访问时间和存储容量之间的一个很好的权衡,过大会导致增加查询成本,过小会导致空间开销增加
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//把节点从链表节点转成树节点的阈值 为什么是8? 后续应该会写到 这是一道面试题
static final int TREEIFY_THRESHOLD = 8;
//从数转为链表的阈值 由于树节点的大小通常是普通节点的两倍,
//当容器中节点数量变小的时候需要被转换回普通的箱子
static final int UNTREEIFY_THRESHOLD = 6;
//当哈希表中的容量大于这个值时,表中的bucket才能进行树形化
//为了避免进行扩容、树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD
static final int MIN_TREEIFY_CAPACITY = 64;
//存放bucket的数组
transient Node[] table;
//目前也没有发现有什么用
transient Set> entrySet;
//size表示table的实际元素数量 而CAPACITY则表示table的容量
transient int size;
//在结构上被修改的次数 用于支持fail-fast机制
transient int modCount;
//扩容临界值 = DEFAULT_INITIAL_CAPACITY * loadFactor
// 按照默认值来计算就是 16 * .75 = 12(临界值)
int threshold;
//加载因子 默认值是 DEFAULT_LOAD_FACTOR .75f
final float loadFactor;
HashMap相比较于List系列来说要复杂很多,毕竟链表+数组+红黑树远比List的数组或链表实现要复杂的多,一定要认真看
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
①:得到key的hashCode值h
②:h异或自己右移16位的结果
③:h右移16位是为了防止h的高位不同,但低位相同的导致的hash冲突,即在后面的与运算中将只有低位参与运算,这个操作将尽量做到任何一位的变化都会对结果产生影响以降低碰撞的概率,这个操作称之为扰动
作用:扩容或初始化表
/**
* 1.初始化表
* 2.扩容表
*/
final Node[] resize () {
//扩容前哈希表 下文简称旧表
Node[] oldTab = table;
//获取旧表的容量 也可能是null null下面则初始化表
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//获取扩容阈值 注意第一次put时它并没有经过DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY计算 是构造中指定的值
int oldThr = threshold;
//新表容量 新表阈值
int newCap, newThr = 0;
//旧表容量大于0
if (oldCap > 0) {
//判断旧表容量是否大于表的max容量 如果大于则不再扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//新表容量为旧表容量的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//阈值也为原来的两倍
newThr = oldThr << 1; // double threshold
//初始化哈希表 构造函数中指定值或者默认值都需要初始化
} 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);
}
//如上面的else没走 这里需要计算新的阀值 只有oldThr容量>0 下面才会执行 否则使用上面else的默认值
if (newThr == 0) {
float ft = (float) newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
(int) ft : Integer.MAX_VALUE);
}
//记录下扩容阈值
threshold = newThr;
//根据新的长度创建新数组
Node[] newTab = (Node[]) new Node[newCap];
table = newTab;
//将旧表中的所有bucket移动到新表中
//如果某个bucket下的链表已转为红黑树,且树的节点少于UNTREEIFY_THRESHOLD将触发红黑树转链表
if (oldTab != null) {
作用:将指定值与指定键关联,如果此映射中已经包含键的映射,则替换旧值
//如果哈希表为null或者长度为0就需要扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//计算index=(length - 1) & hash 获取该位置的节点 数组中不存在则新建节点
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//如果从index位置获取到节点不为Null, 即说明存在hash冲突
//判断是否是hash值相同或key相同 它必须要满足key也相同的判断 否则后续是不能直接替换value的
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//记录下这个节点 后续直接替换value
e = p;
//如果是红黑树(TreeNode) 存在两种情况
//1.key已经存在这个红黑树当中了,更新value
//2.从红黑树的root节点开始遍历,定位到要插入的叶子节点,插入新节点
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
else {
//遍历该链表的所有节点 如果长度>=8 则存在两种情况
//1.该链表长度>=8 哈希表长度<64 则执行扩容
//2.该链表长度>=8 哈希表长度>64 则执行链表转红黑树
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;
}
}
//如果上面的操作中存在key相同的情况 则value直接覆盖
if (e != null) { // existing mapping for key
V oldValue = e.value;
}
}
//结构修改次数
++modCount;
//判断是否达到扩容临界值
if (++size > threshold)
//扩容
resize();
作用: 删除映射中键对应的映射,如果键存在!返回键对应的值,如果不存在返回null
//计算index (n - 1) & hash 获取表中index位置的元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node node = null, e;
K k;
V v;
//hash一致key一致 将直接返回此node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//一个 桶 中有多个节点的情况 通过循环遍历再根据key获取到指定节点
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode) p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//返回上面获取到的节点
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
//如果超过8 则已经树形化 通过红黑树的remove方法来删除此节点
//removeTreeNode还有一个额外的判断 如果树上的节点太少 红黑树将转为链表
((TreeNode) node).removeTreeNode(this, tab, movable);
else if (node == p)
//桶中只有一个节点的情况下 使用null替换该位置
tab[index] = node.next;
else
//桶中有多节点情况 但尚未超过8:当node节点被获取后 node的下一个节点替换node节点的位置
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
作用:从表中获取对应的node,是replace/get等方法的核心
Node[] tab; Node first, e; int n; K k;
//计算index 从表中对应index位置获取node节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//判断该节点是否满足条件 满足则直接返回
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//上一步没有返回 说明桶中有多个节点 通过循环使用key判断获取到对应的节点返回
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
作用:替换指定键映射的值,如果键存在!相当于map.put(key, value)
Node e;
//获取node
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
//替换此节点的value字段
e.value = value;
afterNodeAccess(e);
//返回替换前的value
return oldValue;
}
return null;
作用:返回指定键映射到的值
return (e = getNode(hash(key), key)) == null ? null : e.value;
…
使用entrySet()你需要了解:
作用:返回一个EntrySet对象,以便于操作HashMap迭代
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator> iterator() {
return new EntryIterator();
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry) o;
Object key = e.getKey();
Node candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
public final boolean remove(Object o) {
if (o instanceof Map.Entry) {
Map.Entry e = (Map.Entry) o;
Object key = e.getKey();
Object value = e.getValue();
return removeNode(hash(key), key, value, true, true) != null;
}
return false;
}
通过部分源码可以看出它的功能是由HashMap来支持的,重点只关注一个方法iterator(),EntryIterator继承了HashIterator,这个类在 keySet()、values()中返回的对象的迭代都继承自它,所以这里说一遍下面就简单了
#####变量
//下一个被返回的节点
Node next; // next entry to return
//当前节点
Node current; // current entry
//结构被修改次数
int expectedModCount; // for fast-fail
//当前当前节点在table数组中的索引
int index; // current slot
#####构造函数
HashIterator() {
expectedModCount = modCount;
//table也就是HashMap的table HashIterator是HashMap的非静态内部类
Node[] t = table;
current = next = null;
//索引从0开始
index = 0;
if (t != null && size > 0) { // advance to first entry
//从索引0开始 依次遍历 直到获取第一个不为null的节点 停止while循环
do {} while (index < t.length && (next = t[index++]) == null);
}
}
final Node nextNode () {
Node[] t;
Node e = next;
//判断桶内是否是否有多个节点 通过next = (current = e).next可以依次获取桶内的节点返回
//如果桶内是单节点 则下面通过while循环查找到下一个不为null的节点 可能有点拗口 下面举个例子说明
//假设现在table=如下图,桶0是一个单链表,它有多个节点,
//桶1则只有一个节点,假设在上面的构造函数中 next= a,这里if里会变成 e= a,current = e = a, next = a.next = b
//很明显b不为null,这个判断也就不成立 下面直接return的是a 此时用户调用next()方法,这时 e= b,current = b next = c
//很明显c不为null,这个判断也就不成立 下面直接return的是b 此时用户调用next()方法,这时 e= c,current = c next = null
//此时桶0的所有节点都已经拿到,next为null判断成立,在while中获取下一个桶1
//....
if ((next = (current = e).next) == null && (t = table) != null) {
//桶 内不存在多个节点 执行while循环 直到获取下一个不为null的节点
do {
} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
作用:返回HashMap所有键的Set视图
不贴代码了,写的我脑壳疼,说下和entrySet()方法的区别吧
return nextNode().key;
return nextNode();
如果你打开了源码,你应该可以看到不管是keySet还是entrySet它们返回的迭代器都继承自HashIterator,nextNode方法都是一致的,区别仅仅是一个直接返回一个是返回key,如果你非要使用keySet获取key再通过get(key)获取value,那么keySet的效率肯定要低于entrySet,或者说一般情况下我们使用entrySet就
作用:返回HashMap所有值的Collection视图,注意和前两个方法不同,Values不继承自Set,而是Collection,也许你会有疑问(对没错,就是我自己)为什么不是Set了?因为Value的视图不符合Set的唯一思想,Value是可重复的,使用Set显然不再适合
return nextNode().value;
使用网上的表述方法不太准确,如果bucket中的元素个数超过TREEIFY_THRESHOLD(8),但是哈希表的容量没有超过64时是不会链表转红黑树的,而是扩容!(我也很怕自己以偏概全.遂反复通过Demo及源码验证.如有错误请提示下)
什么是哈希?
什么是散列碰撞?
如何解决散列碰撞?
为什么bucket中个数超过8才转为红黑树,为什么是8?
先要知道TreeNodes占用空间是普通Nodes的两倍,所以只有当链表的节点数量足够多时才会转换为红黑树,当链表的节点减少到6时再树转链表,过早就浪费空间开销增加,过晚会导致查询时间成本增加,在源码中有一段注释:
//在扩容阈值为0.75的情况下,遵循着参数平均为0.5的泊松分布
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
可以看到一个链表的节点要达到8的概率为0.00000006,所以选择8是在衡量了时间与空间之后通过概率统计决定的值
为什么容量是2的幂次方?
为什么默认加载因子是.75f
HashMap中有哪些影响其性能的参数?
什么是桶?经常看到有文章里写桶神马的.桶在HashMap中扮演什么角色?
2019/6/17 23:20:57