代码版本: JDK 1.8
都是集合框架,和上篇文章思路一样。
介绍: HashMap底层实现由之前的【数组+链表(1.7之前)】改为【数组+链表+红黑树(1.8)】。看到数组和链表是不是有点似曾相识,没错,ArrayList LinkedList就用了这俩东西,表面是要学习个新的集合框架,但是却只换了个皮。废话不多说了,看源码。
- 构造函数(先看两个构造函数)
// 当然先看最简单的了,就一句话
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 初始化一个负载因子,这是干啥的??后面再说👎
}
// 说是两个,其实是三个 ^
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY) // [MAXIMUM_CAPACITY = 1 << 30]
initialCapacity = MAXIMUM_CAPACITY; // 只能装这么多
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor; // 初始化一个负载因子,这是干啥的??后面再说👎
this.threshold = tableSizeFor(initialCapacity); // 临界值(容量*负载因数)
}
- 增加元素
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// 这一坨代码,看起来是不是有点晕 +_+ 先放在这不看,略过此段代码!!
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)
n = (tab = resize()).length;
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);
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;
}
}
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;
}
增加第一个元素
// 现在只看增加第一个元素时走的代码逻辑,是不是心情一下就变好了 ^_
// 此番逻辑我只是按照在第一次添加元素的时候走的
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; //定义底层数组 这就是hashmap底层的数组了
Node<K,V> p; // 链表
int n, i;
if ((tab = table) == null || (n = tab.length) == 0) // 第一次添加元素,table为null
// 👇 快去看下resize()方法, ▲▲ 看完resize()继续看这里,此时n = 16了
n = (tab = resize()).length; // 集合大小
// 不看resize不知道tab是啥,那就看吧(又是一坨Ⅹ)
// n-1 为啥馁? 因为计算机扩容基本都是二倍增长的,所以这里为了增加hash散列度
if ((p = tab[i = (n - 1) & hash]) == null)
//他来了他来了!i 是hash后的一个值,就是经常说的取模运算了。并实例化新节点,给tab
tab[i] = newNode(hash, key, value, null);
return null;
}
// 老规矩,减小难度,还是只看第一步走的逻辑(多余代码我删除了),那就开始吧
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 此时table还是null,所以oldTab也是null
int oldCap = (oldTab == null) ? 0 : oldTab.length; // 这里就是0了
int oldThr = threshold; // 因为我们使用的是无参构造,这里也是默认的,所以还是0,简单吧
int newCap, newThr = 0; // 还是 0
// 这里有点突如其来的感觉,是因为很多没走的判断被我干掉了
// 默认初始容量 1 << 4 = 16 第一个不是0的地方,注意喽,面试可能会用到。
newCap = DEFAULT_INITIAL_CAPACITY;
// 在第三个构造方法时提到的 负载因子(默认0.75)和临界值(集合扩容的标志)
// 0.75 * 16 = 【12】
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
// 新的临界值赋值给临界值属性,此处是默认情况下,所以为12
threshold = newThr;
// ★★★ 初始化一个大小为16的Node数组,赋值给table并返回
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
return newTab; // 👆 回去继续看putVal()方法了。
}
至此,第一个元素就加进去了。 总结一下:
增加第一次元素最重要的地方以下几点:
- 默认初始容量是数组的容量为16,扩容会根据map的size和threshold比较做扩容
- 默认的负载因子是0.75f
- 临界值为【容量*负载因子】
- 最后一个是hash取模运算那里
下面看添加第二个元素 —> 开始
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 ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
return null;
}
第二轮就是到此一游,啥收获没有。。
继续。我们的目标是hash碰撞,看看这么高大尚的东西到底是个啥东西。
经过一番寻找,终于找到碰撞的key了 【在容量为16的情况下 碰撞的key 2,18,34】
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 ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else { // 发生碰撞 走else代码块
Node<K,V> e; // 这是装碰撞节点的东西
K k; // 存被碰瓷的那个key
// 此时p是被碰瓷的那个节点
// 这个if判断的是 如果 hash相同 并且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) {
// 循环找到碰瓷的最后一个节点,就是单链表的最后一个,因为hash碰撞是在尾部添加的
// 最后一个节点才会进入if
if ((e = p.next) == null) {
// 碰瓷成功,这是追尾了,跟在屁股后面挂着
p.next = newNode(hash, key, value, null);
// 这里转换成的红黑树。 ★,这次先这样,在搞一坨恐怕受不了。无视它
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
// 碰完了,就赶紧跑路吧。
break;
}
/** 如果循环到了最后一个节点,此代码不会执行了。
* 走到这,e不是空值了。
* 这里遇到相同的key就会停止循环,去走覆盖值的逻辑。
*/
if (e.hash == hash &&((k = e.key)== key || (key != null && key.equals(k))))
break;
p = e; // 相当于把p.next赋值给p (e = p.next)
}
}
// 存在相同的key 覆盖相同key e在这里如果不为空,那就是碰到了key相同的情况了
// e的值就是与 来碰瓷的那个key值相同的那个节点,
if (e != null) {
V oldValue = e.value;
// onlyIfAbsent一般都是false,覆盖,putIfAbsent() 这个方法是true,这里不展开了
if (!onlyIfAbsent || oldValue == null)
e.value = value; // 覆盖。
afterNodeAccess(e);
return oldValue;
}
}
return null;
}
至此,hash碰撞算是完了。
再来总结一番:
- 碰瓷的太多的时候,就会生成红黑树。 临界值是TREEIFY_THRESHOLD = 8
- 碰瓷的都是追尾模式,就是在last节点添加元素。
- key相同(覆盖原来的值),---在这里面putIfAbsent()应该比较特殊。
- 采用的一个无限for循环,处理链表碰撞的判断,碰到key相同的,覆盖,不相同的继续循环,直至最后一个节点,在屁股上添加新的节点。
把找相同hash key的代码贴一下
public static void main(String[] args) {
for (int i =0; i<200;i++) {
if (2 == (15 & hash(i))) { // 这个15是map容量-1
System.out.print(i + " ");
}
}
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
输出::::
2 18 34 50 66 82 98 114 130 146 162 178 194
待续...
接下来还有 >>>>>>>>>>>>>>>
- 红黑树
- 查询元素
- 删除元素
- 修改元素