Pasted image 20240716200049

复杂度

Pasted image 20240716201547

List

为什么数组索引从0开始?从1开始不行吗?

Pasted image 20240716201957

ArrayList源码分析

源码基于jdk1.8

成员变量

Pasted image 20240716202844

构造函数

Pasted image 20240716203518
Pasted image 20240716203849

添加和扩容操作

  • 第一次添加数据,自动扩容到10

Pasted image 20240716204847

  • 第2次-10次
    Pasted image 20240716205149

  • 第十一次次添加数据-扩容
    Pasted image 20240716205318

ArrayList底层的实现原理是什么

Pasted image 20240716205449

ArrayList list = new ArrayList(10)中的list扩容几次

Pasted image 20240716205548

该构造函数只是声明和实例了一个ArrayList,指定了容量为10,未扩容

如何实现数组和List之间的转换

Pasted image 20240716210306

Pasted image 20240716210322

用Arrays.asList转List后,如果修改了数组内容,list受影响吗?

受影响

Pasted image 20240716210957
只涉及了引用,没有创建数组

List用toArray转数组后,如果修改了List内容,数组受影响吗?

不受影响
Pasted image 20240716211119
复制了数组中的内容到新数组中
Pasted image 20240716210829

Pasted image 20240716211202

ArrayList和LinkedList的区别是什么?

  • 底层数据结构
    Pasted image 20240716214749
  • 效率
    Pasted image 20240716214802
  • 空间
    Pasted image 20240716214829
  • 线程是否安全
    Pasted image 20240716214850

HashMap

Pasted image 20240716223215

hashmap的实现原理

hashmap的数据结构:底层使用hash表数据结构,即数组和链表和红黑树

Pasted image 20240717085033

hashmap的jdk1.7和jdk1.8有什么区别?

Pasted image 20240717085109

hashmap的put方法的具体流程?

常见属性

Pasted image 20240717092638

当put元素时

Pasted image 20240717092757

  • hashmap是懒加载,在创建对象时并没有初始化数组
  • 在无参的构造函数中,设置了默认的加载因子是0.75

Pasted image 20240717094326

put方法具体流程

Pasted image 20240717094704

hashmap的扩容机制

Pasted image 20240717103900

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//扩容、初始化数组
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//如果当前数组为null的时候,把oldCap老数组容量设置为0
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//老的扩容阈值
int oldThr = threshold;
int newCap, newThr = 0;
//判断数组容量是否大于0,大于0说明数组已经初始化
if (oldCap > 0) {
//判断当前数组长度是否大于最大数组长度
if (oldCap >= MAXIMUM_CAPACITY) {
//如果是,将扩容阈值直接设置为int类型的最大数值并直接返回
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果在最大长度范围内,则需要扩容 OldCap << 1等价于oldCap*2
//运算过后判断是不是最大值并且oldCap需要大于16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold 等价于oldThr*2
}
//如果oldCap<0,但是已经初始化了,像把元素删除完之后的情况,那么它的临界值肯定还存在, 如果是首次初始化,它的临界值则为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);
}
//初始化容量小于16的时候,扩容阈值是没有赋值的
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;
//扩容操作,判断不为空证明不是初始化数组
if (oldTab != null) {
//遍历数组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//判断当前下标为j的数组如果不为空的话赋值个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 {
//比如老数组容量是16,那下标就为0-15
//扩容操作*2,容量就变为32,下标为0-31
//低位:0-15,高位16-31
//定义了四个变量
// 低位头 低位尾
Node<K,V> loHead = null, loTail = null;
// 高位头 高位尾
Node<K,V> hiHead = null, hiTail = null;
//下个节点
Node<K,V> next;
//循环遍历
do {
//取出next节点
next = e.next;
//通过 与操作 计算得出结果为0
if ((e.hash & oldCap) == 0) {
//如果低位尾为null,证明当前数组位置为空,没有任何数据
if (loTail == null)
//将e值放入低位头
loHead = e;
//低位尾不为null,证明已经有数据了
else
//将数据放入next节点
loTail.next = e;
//记录低位尾数据
loTail = e;
}
//通过 与操作 计算得出结果不为0
else {
//如果高位尾为null,证明当前数组位置为空,没有任何数据
if (hiTail == null)
//将e值放入高位头
hiHead = e;
//高位尾不为null,证明已经有数据了
else
//将数据放入next节点
hiTail.next = e;
//记录高位尾数据
hiTail = e;
}

}
//如果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;
}

讲一讲hashmap的扩容机制
Pasted image 20240717111543

hashmap的寻址算法

Pasted image 20240717130738

计算对象的hashCode()
再调用hash()方法进行二次哈希,hashcode值右移16位再异或运算,让哈希分布更为均匀
最后(capacity-1)&hash得到索引

为什么hashmap的数组长度一定是2的次幂?

Pasted image 20240717131211

hashmap在1.7情况下的多线程死循环问题

Pasted image 20240717131910

Pasted image 20240717132038
举例:线程1和线程2的变量e和next都引用了这两个节点
Pasted image 20240717133628
线程2扩容后,由于头插法,链表顺序颠倒,但是线程1的临时变量e和next还引用了这个两个节点
Pasted image 20240717134517
第一次while循环,因为此时(上图)e指向A,所以A进行迁移,那么下一个e的位置指向此时的next位置,也就是B,而next下一个位置则指向现在next节点的next,也就是A

Pasted image 20240717134644

第二次循环,B进行迁移,e节点的下一个位置则是next节点,也就是A;next节点的下一个位置则是此时next节点的下一个next,null
Pasted image 20240717135005

第三次循环,此时e指向的还是A,进行迁移,头插法,e的下一个位置是null;next也是null,出现循环
Pasted image 20240717135146

参考问答

Pasted image 20240717135508