基础知识
合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下。

更新于 

这是分页标题

JDK源码

数组ArrayList

1.类图

ArrayList 实现的接口、继承的抽象类,如下图所示:

image-20230401092426572

实现了 4 个接口,分别是:

  • java.util.List 接口,提供数组的添加、删除、修改、迭代遍历等操作。
  • java.util.RandomAccess 接口,表示 ArrayList 支持快速的随机访问。
  • java.io.Serializable 接口,表示 ArrayList 支持序列化的功能。
  • java.lang.Cloneable 接口,表示 ArrayList 支持克隆。

继承了java.util.AbstractList 抽象类,而 AbstractList 提供了 List 接口的骨架实现,大幅度的减少了实现迭代遍历相关操作的代码

2.属性

ArrayList 的属性很少,仅仅 2 个。如下图所示:

image-20230401092935220

  • elementData 属性:元素数组。其中,图中红色空格代表我们已经添加元素,白色空格代表我们并未使用。
  • size 属性:数组大小。注意,size 代表的是 ArrayList 已使用 elementData 的元素的数量,对于开发者看到的 #size() 也是该大小。并且,当我们添加新的元素时,恰好其就是元素添加到 elementData 的位置(下标)。当然,我们知道 ArrayList 真正的大小是 elementData 的大小。

对应代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ArrayList.java

/**
* 元素数组。
*
* 当添加新的元素时,如果该数组不够,会创建新数组,并将原数组的元素拷贝到新数组。之后,将该变量指向新数组。
*
* The array buffer into which the elements of the ArrayList are stored.
* The capacity of the ArrayList is the length of this array buffer. Any
* empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
* will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access 不使用 private 修饰,方便内嵌类的访问。

/**
* 已使用的数组大小
*
* The size of the ArrayList (the number of elements it contains).
*
* @serial
*/
private int size;

3.构造方法

ArrayList 一共有三个构造方法:

#ArrayList(int initialCapacity)

#ArrayList(int initialCapacity) 构造方法,根据传入的初始化容量,创建 ArrayList 数组。如果我们在使用时,如果预先指到数组大小,一定要使用该构造方法,可以避免数组扩容提升性能,同时也是合理使用内存。代码如下:

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
// ArrayList.java

/**
* 共享的空数组对象。
*
* 在 {@link #ArrayList(int)} 或 {@link #ArrayList(Collection)} 构造方法中,
* 如果传入的初始化大小或者集合大小为 0 时,将 {@link #elementData} 指向它。
*
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};

public ArrayList(int initialCapacity) {
// 初始化容量大于 0 时,创建 Object 数组
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
// 初始化容量等于 0 时,使用 EMPTY_ELEMENTDATA 对象
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
// 初始化容量小于 0 时,抛出 IllegalArgumentException 异常
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
  • 如果初始化容量为 0 时,使用 EMPTY_ELEMENTDATA 空数组。在添加元素的时候,会进行扩容创建需要的数组。

#ArrayList(Collection<? extends E> c)

#ArrayList(Collection<? extends E> c) 构造方法,使用传入的 c 集合,作为 ArrayList 的 elementData 。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ArrayList.java

public ArrayList(Collection<? extends E> c) {
// 将 c 转换成 Object 数组
elementData = c.toArray();
// 如果数组大小大于 0
if ((size = elementData.length) != 0) {
// defend against c.toArray (incorrectly) not returning Object[]
// (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
// <X> 如果集合元素不是 Object[] 类型,则会创建新的 Object[] 数组,并将 elementData 赋值到其中,最后赋值给 elementData 。
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
// 如果数组大小等于 0 ,则使用 EMPTY_ELEMENTDATA 。
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
  • <X> 处的代码。它是用于解决 JDK-6260652 的 Bug 。它在 JDK9 中被解决,也就是说,JDK8 还会存在该问题。

看一段能够触发 JDK-6260652 的测试代码,然后分别在 JDK8 和 JDK13 下执行。代码如下:

1
2
3
4
5
6
7
8
9
10
// ArrayListTest.java

public static void test02() {
List<Integer> list = Arrays.asList(1, 2, 3);
Object[] array = list.toArray(); // JDK8 返回 Integer[] 数组,JDK9+ 返回 Object[] 数组。
System.out.println("array className :" + array.getClass().getSimpleName());

// 此处,在 JDK8 和 JDK9+ 表现不同,前者会报 ArrayStoreException 异常,后者不会。
array[0] = new Object();
}

JDK8 执行如下图所示:

image-20230401094902581

JDK13 执行如下图所示:

image-20230401094952458

在 JDK8 中,返回的实际是 Integer [] 数组,那么我们将 Object 对象设置到其中,肯定是会报错的。

#ArrayList()

无参数构造方法 #ArrayList() 构造方法,也是我们使用最多的构造方法。代码如下:

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
// ArrayList.java

/**
* 默认初始化容量
*
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;

/**
* 共享的空数组对象,用于 {@link #ArrayList()} 构造方法。
*
* 通过使用该静态变量,和 {@link #EMPTY_ELEMENTDATA} 区分开来,在第一次添加元素时。
*
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
  • 在学习 ArrayList 的时候,认为在未设置初始化容量时,ArrayList 默认大小为 10 。但是此处,可以看到初始化为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 这个空数组。这是为什么呢?ArrayList 考虑到节省内存,一些使用场景下仅仅是创建了 ArrayList 对象,实际并未使用。所以,==ArrayList 优化成初始化是个空数组,在首次添加元素时,才真正初始化为容量为 10 的数组==。
  • 那么为什么单独声明了 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 空数组,而不直接使用 EMPTY_ELEMENTDATA 呢?DEFAULTCAPACITY_EMPTY_ELEMENTDATA首次扩容为 10 ,而EMPTY_ELEMENTDATA` 按照 1.5 倍扩容从 0 开始而不是 10 。

4.添加单个元素

① 顺序添加单个元素

#add(E e) 方法,顺序添加单个元素到数组。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ArrayList.java

@Override
public boolean add(E e) {
// <1> 增加数组修改次数
modCount++;
// 添加元素
add(e, elementData, size);
// 返回添加成功
return true;
}

private void add(E e, Object[] elementData, int s) {
// <2> 如果容量不够,进行扩容
if (s == elementData.length)
elementData = grow();
// <3> 设置到末尾
elementData[s] = e;
// <4> 数量大小加一
size = s + 1;
}
  • <1> 处,增加数组修改次数 modCount 。在父类 AbstractList 上,定义了 modCount 属性,用于记录数组修改次数。
  • <2> 处,如果元素添加的位置就超过末尾(数组下标是从 0 开始,而数组大小比最大下标大 1),说明数组容量不够,需要进行扩容,那么就需要调用 #grow() 方法,进行扩容。
  • <3> 处,设置到末尾。
  • <4> 处,数量大小加一。

② 插入单个元素到指定位置

再看看 #add(int index, E element) 方法,插入单个元素到指定位置。代码如下:

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
// ArrayList.java

public void add(int index, E element) {
// 校验位置是否在数组范围内
rangeCheckForAdd(index);
// 增加数组修改次数
modCount++;
// 如果数组大小不够,进行扩容
final int s;
Object[] elementData;
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
// 将 index + 1 位置开始的元素,进行往后挪
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
// 设置到指定位置
elementData[index] = element;
// 数组大小加一
size = s + 1;
}

private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

6.数组扩容

#grow() 方法,扩容数组,并返回它。整个的扩容过程,首先创建一个新的更大的数组,一般是 1.5 倍大小(为什么说是一般呢,稍后会看到,会有一些小细节),然后将原数组复制到新数组中,最后返回新数组。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ArrayList.java

private Object[] grow() {
// <1>
return grow(size + 1);
}

private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
// <2> 如果原容量大于 0 ,或者数组不是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,计算新的数组大小,并创建扩容
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
// <3> 如果是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 数组,直接创建新的数组即可。
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
  • <1> 处,调用 #grow(int minCapacity) 方法,要求扩容后至少比原有大 1 。因为是最小扩容的要求,实际是允许比它大。
  • <2>处,如果原容量大于 0 时,又或者数组不是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,则计算新的数组大小,并创建扩容。
    • ArraysSupport#newLength(int oldLength, int minGrowth, int prefGrowth) 方法,计算新的数组大小。简单来说,结果就是 Math.max(minGrowth, prefGrowth) + oldLength ,按照 minGrowthprefGrowth 取大的。
    • 一般情况下,从 oldCapacity >> 1 可以看处,是 1.5 倍扩容。但是会有两个特殊情况: 1)初始化数组要求大小为 0 的时候,0 >> 1 时(>> 1 为右移操作,相当于除以 2)还是 0 ,此时使用 minCapacity 传入的 1 。 2)在下文中,会看到添加多个元素,此时传入的 minCapacity 不再仅仅加 1 ,而是扩容到 elementData 数组恰好可以添加下多个元素,而该数量可能会超过当前 ArrayList 0.5 倍的容量。
  • <3> 处,如果是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 数组,直接创建新的数组即可。思考下,如果无参构造方法使用 EMPTY_ELEMENTDATA 的话,无法实现该效果了。

既然有数组扩容方法,那么是否有缩容方法呢?在 #trimToSize() 方法中,会创建大小恰好够用的新数组,并将原数组复制到其中。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// ArrayList.java

public void trimToSize() {
// 增加修改次数
modCount++;
// 如果有多余的空间,则进行缩容
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA // 大小为 0 时,直接使用 EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size); // 大小大于 0 ,则创建大小为 size 的新数组,将原数组复制到其中。
}
}

同时,提供 #ensureCapacity(int minCapacity) 方法,保证 elementData 数组容量至少有 minCapacity 。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// ArrayList.java

public void ensureCapacity(int minCapacity) {
if (minCapacity > elementData.length // 如果 minCapacity 大于数组的容量
&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
&& minCapacity <= DEFAULT_CAPACITY)) { // 如果 elementData 是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的时候,
// 需要最低 minCapacity 容量大于 DEFAULT_CAPACITY ,因为实际上容量是 DEFAULT_CAPACITY 。
// 数组修改次数加一
modCount++;
// 扩容
grow(minCapacity);
}
}

可以将这个方法理解成主动扩容。

7.添加多个元素

#addAll(Collection<? extends E> c) 方法,批量添加多个元素。在我们明确知道会添加多个元素时,推荐使用该该方法而不是添加单个元素,避免可能多次扩容。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ArrayList.java

public boolean addAll(Collection<? extends E> c) {
// 转成 a 数组
Object[] a = c.toArray();
// 增加修改次数
modCount++;
// 如果 a 数组大小为 0 ,返回 ArrayList 数组无变化
int numNew = a.length;
if (numNew == 0)
return false;
// <1> 如果 elementData 剩余的空间不够,则进行扩容。要求扩容的大小,至于能够装下 a 数组。
Object[] elementData;
final int s;
if (numNew > (elementData = this.elementData).length - (s = size))
elementData = grow(s + numNew);
// <2> 将 a 复制到 elementData 从 s 开始位置
System.arraycopy(a, 0, elementData, s, numNew);
// 数组大小加 numNew
size = s + numNew;
return true;
}
  • <1> 处,如果 elementData 剩余的空间不足,则进行扩容。要求扩容的大小,至于能够装下 a 数组。当然,在 ==6. 数组扩容== 的小节,我们已经看到,如果要求扩容的空间太小,则扩容 1.5 倍
  • <2> 处,将 a 复制到 elementDatas 开始位置。

再来看看 #addAll(int index, Collection<? extends E> c) 方法,从指定位置开始插入多个元素。代码如下:

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
// ArrayList.java

public boolean addAll(int index, Collection<? extends E> c) {
// 校验位置是否在数组范围内
rangeCheckForAdd(index);

// 转成 a 数组
Object[] a = c.toArray();
// 增加数组修改次数
modCount++;
// 如果 a 数组大小为 0 ,返回 ArrayList 数组无变化
int numNew = a.length;
if (numNew == 0)
return false;
// 如果 elementData 剩余的空间不够,则进行扩容。要求扩容的大小,至于能够装下 a 数组。
Object[] elementData;
final int s;
if (numNew > (elementData = this.elementData).length - (s = size))
elementData = grow(s + numNew);

// 【差异点】如果 index 开始的位置已经被占用,将它们后移
int numMoved = s - index;
if (numMoved > 0)
System.arraycopy(elementData, index,
elementData, index + numNew,
numMoved);

// 将 a 复制到 elementData 从 s 开始位置
System.arraycopy(a, 0, elementData, index, numNew);
// 数组大小加 numNew
size = s + numNew;
return true;
}

判断添加元素的位置,如果当前数组大小减去需要添加的位置下标大于零,说明该位置有元素,那么久进行移动,移动添加元素的个数位,然后将新添加的元素指定位置开始从复制到数组中。

8.移除单个元素

#remove(int index) 方法,移除指定位置的元素,并返回该位置的原元素。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ArrayList.java

public E remove(int index) {
// 校验 index 不要超过 size
Objects.checkIndex(index, size);
final Object[] es = elementData;

// 记录该位置的原值
@SuppressWarnings("unchecked") E oldValue = (E) es[index];
// <X>快速移除
fastRemove(es, index);

// 返回该位置的原值
return oldValue;
}

重点是 <X> 处,调用 #fastRemove(Object[] es, int i) 方法,快速移除。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
// ArrayList.java

private void fastRemove(Object[] es, int i) {
// 增加数组修改次数
modCount++;
// <Y>如果 i 不是移除最末尾的元素,则将 i + 1 位置的数组往前挪
final int newSize;
if ((newSize = size - 1) > i) // -1 的原因是,size 是从 1 开始,而数组下标是从 0 开始。
System.arraycopy(es, i + 1, es, i, newSize - i);
// 将新的末尾置为 null ,帮助 GC
es[size = newSize] = null;
}

#remove(Object o) 方法,移除首个为 o 的元素,并返回是否移除到。代码如下:

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
// ArrayList.java

public boolean remove(Object o) {
final Object[] es = elementData;
final int size = this.size;
// <Z> 寻找首个为 o 的位置
int i = 0;
found: {
if (o == null) { // o 为 null 的情况
for (; i < size; i++)
if (es[i] == null)
break found;
} else { // o 非 null 的情况
for (; i < size; i++)
if (o.equals(es[i]))
break found;
}
// 如果没找到,返回 false
return false;
}
// 快速移除
fastRemove(es, i);
// 找到了,返回 true
return true;
}

#remove(int index) 差不多,就是在 <Z> 处,改成获得首个为 o 的位置,之后就调用 #fastRemove(Object[] es, int i) 方法,快速移除即可。

9. 移除多个元素

先看 #removeRange(int fromIndex, int toIndex) 方法,批量移除 [fromIndex, toIndex) 的多个元素,注意不包括 toIndex 的元素噢。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ArrayList.java

protected void removeRange(int fromIndex, int toIndex) {
// 范围不正确,抛出 IndexOutOfBoundsException 异常
if (fromIndex > toIndex) {
throw new IndexOutOfBoundsException(
outOfBoundsMsg(fromIndex, toIndex));
}
// 增加数组修改次数
modCount++;
// <X> 移除 [fromIndex, toIndex) 的多个元素
shiftTailOverGap(elementData, fromIndex, toIndex);
}

private static String outOfBoundsMsg(int fromIndex, int toIndex) {
return "From Index: " + fromIndex + " > To Index: " + toIndex;
}

<X> 处,调用 #shiftTailOverGap(Object[] es, int lo, int hi) 方法,移除 [fromIndex, toIndex) 的多个元素。代码如下:

1
2
3
4
5
6
7
8
9
// ArrayList.java

private void shiftTailOverGap(Object[] es, int lo, int hi) {
// 将 es 从 hi 位置开始的元素,移到 lo 位置开始。
System.arraycopy(es, hi, es, lo, size - hi);
// 将从 [size - hi + lo, size) 的元素置空,因为已经被挪到前面了。
for (int to = size, i = (size -= hi - lo); i < to; i++)
es[i] = null;
}
  • #fastRemove(Object[] es, int i) 方法一样的套路,先挪后置 null
  • 有一点要注意,ArrayList 特别喜欢把多行代码写成一行。所以,可能疑惑,貌似这里没有修改数组的大小 size 啊?答案在 i = (size -= hi - lo) ,简直到精简到难懂。

#removeAll(Collection<?> c) 方法,批量移除指定的多个元素。简单来说,通过两个变量 w(写入位置)和 r(读取位置),按照 r 顺序遍历数组(elementData),如果不存在于指定的多个元素中,则写入到 elementDataw 位置,然后 w 位置 + 1 ,跳到下一个写入位置。通过这样的方式,实现将不存在 elementData 覆盖写到 w 位置。代码如下:

1
2
3
4
5
// ArrayList.java

public boolean removeAll(Collection<?> c) {
return batchRemove(c, false, 0, size);
}

调用 #batchRemove(Collection<?> c, boolean complement, final int from, final int end) 方法,批量移除指定的多个元素。代码如下:

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
// ArrayList.java

boolean batchRemove(Collection<?> c, boolean complement, final int from, final int end) {
// 校验 c 非 null 。
Objects.requireNonNull(c);
final Object[] es = elementData;
int r;
// Optimize for initial run of survivors
// <1> 优化,顺序遍历 elementData 数组,找到第一个不符合 complement ,然后结束遍历。
for (r = from;; r++) {
// <1.1> 遍历到尾,都没不符合条件的,直接返回 false 。
if (r == end)
return false;
// <1.2> 如果包含结果不符合 complement 时,结束
if (c.contains(es[r]) != complement)
break;
}
// <2> 设置开始写入 w 为 r ,注意不是 r++ 。
// r++ 后,用于读取下一个位置的元素。因为通过上的优化循环,我们已经 es[r] 是不符合条件的。
int w = r++;
try {
// <3> 继续遍历 elementData 数组,如何符合条件,则进行移除
for (Object e; r < end; r++)
if (c.contains(e = es[r]) == complement) // 判断符合条件
es[w++] = e; // 移除的方式,通过将当前值 e 写入到 w 位置,然后 w 跳到下一个位置。
} catch (Throwable ex) {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
// <4> 如果 contains 方法发生异常,则将 es 从 r 位置的数据写入到 es 从 w 开始的位置
System.arraycopy(es, r, es, w, end - r);
w += end - r;
// 继续抛出异常
throw ex;
} finally { // <5>
// 增加数组修改次数
modCount += end - w;
// 将数组 [w, end) 位置赋值为 null 。
shiftTailOverGap(es, w, end);
}
return true;
}
  • complement参数,翻译过来是“补足”的意思。怎么理解呢?表示如果elementData元素在c集合中时,是否保留。
    • 如果 complementfalse 时,表示在集合中,就不保留,这显然符合 #removeAll(Collection<?> c) 方法要移除的意图。
    • 如果 complementtrue 时,表示在集合中,就暴露,这符合我们后面会看到的 #retainAll(Collection<?> c) 方法要求交集的意图。
  • <1>处,首先要知道这是一个基于 Optimize 优化的目的。我们是希望先判断是否elementData没有任何一个符合c的,这样就无需进行执行对应的移除逻辑。但是,又希望能够避免重复遍历,于是就有了这样一块的逻辑。总的来说,这块逻辑的目的是,优化,顺序遍历elementData数组,找到第一个不符合complement,然后结束遍历。
    • <1.1> 处,遍历到尾,都没不符合条件的,直接返回 false 。也就是说,丫根就不需要进行移除的逻辑。
    • <1.2> 处,如果包含结果不符合 complement 时,结束循环。可能有点难理解,我们来举个例子。假设 elementData[1, 2, 3, 1] 时,c[2] 时,那么在遍历第 0 个元素 1 时,则 c.contains(es[r]) != complement => false != false 不符合,所以继续缓存;然后,在遍历第 1 个元素 2 时,c.contains(es[r]) != complement => true != false 符合,所以结束循环。此时,我们便找到了第一个需要移除的元素的位置。当然,移除不是在这里执行
  • <2> 处,设置开始写入 wr ,注意不是 r++ 。这样,我们后续在循环 elementData 数组,就会从 w 开始写入。并且此时,r 也跳到了下一个位置,这样间接我们可以发现,w 位置的元素已经被“跳过”了。
  • <3> 处,继续遍历 elementData 数组,符合条件,则进行移除。可能有点难理解,继续上述例子。遍历第 2 个元素 3 时候,c.contains(es[r]) == complement => false == false 符合,所以将 3 写入到 w 位置,同时 w 指向下一个位置;遍历第三个元素 1 时候,c.contains(es[r]) == complement => true == false 不符合,所以不进行任何操作。
  • <4> 处,如果 contains 方法发生异常,则将 esr 位置的数据写入到 esw 开始的位置。这样,保证我们剩余未遍历到的元素,能够挪到从从 w 开始的位置,避免多出来一些元素。
  • <5> 处,是不是很熟悉,将数组 [w, end) 位置赋值为 null

精妙之处就在于<3>处,前面拿到第一个符合移除的元素的下标r,将该下标赋值给w,然后再次从r处进行遍历,如果r+1处的元素不是需要移除的,那么就把r+1处的元素赋值给w处(即原来需要移除的元素位置),然后w+1;如此循环,遇到不是需要移除的就向前移动一位,是需要移除的就把当前需要移除的元素以后的元素向前移动一位,只有事不需要移除的元素时,w才会进行++操作,其实w就是记录写入点的。

#retainAll(Collection<?> c) 方法,求 elementData 数组和指定多个元素的交集。简单来说,恰好和 #removeAll(Collection<?> c) 相反,移除不在 c 中的元素。代码如下:

1
2
3
4
5
// ArrayList.java

public boolean retainAll(Collection<?> c) {
return batchRemove(c, true, 0, size);
}

举例:c是[1,2] elementData为[3,1,4,2]则该方法:

  1. 找出第一个不在c中的元素即3的下标0,然后w=0r=1
  2. r=1处遍历,发现elementData中的第二个元素也就是1存在于c中,那么就把1赋值给下标为w=0的位置,此时elementDate为[1,1,4,2];w=0+1
  3. 然后r=2,elementData中元素为4,不在c中,那么不进行赋值,w也不变,还是1
  4. 再次遍历,r=3,elementData中元素为2,在c中,那么把元素2赋值到w=1位置,此时elementData为[1,2,4,2];w为2;
  5. 然后从下标为w=2处开始到size=4的元素赋值为null;此时elementData为[1,2]

10.查找单个元素

  • 查找首个为指定元素的位置

#indexOf(Object o) 方法,查找首个为指定元素的位置,正叙循环筛选。代码如下:

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
// ArrayList.java

public int indexOf(Object o) {
return indexOfRange(o, 0, size);
}

int indexOfRange(Object o, int start, int end) {
Object[] es = elementData;
// o 为 null 的情况
if (o == null) {
for (int i = start; i < end; i++) {
if (es[i] == null) {
return i;
}
}
// o 非 null 的情况
} else {
for (int i = start; i < end; i++) {
if (o.equals(es[i])) {
return i;
}
}
}
// 找不到,返回 -1
return -1;
}
  • #contains(Object o) 方法

#contains(Object o) 方法,就是基于该方法实现。代码如下:

1
2
3
4
5
// ArrayList.java

public boolean contains(Object o) {
return indexOf(o) >= 0;
}
  • 查找最后一个为指定元素的位置

有时我们需要查找最后一个为指定元素的位置,所以会使用到 #lastIndexOf(Object o) 方法,倒叙循环筛选。代码如下:

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
// ArrayList.java

public int lastIndexOf(Object o) {
return lastIndexOfRange(o, 0, size);
}

int lastIndexOfRange(Object o, int start, int end) {
Object[] es = elementData;
// o 为 null 的情况
if (o == null) {
for (int i = end - 1; i >= start; i--) { // 倒序
if (es[i] == null) {
return i;
}
}
// o 非 null 的情况
} else {
for (int i = end - 1; i >= start; i--) { // 倒序
if (o.equals(es[i])) {
return i;
}
}
}

// 找不到,返回 -1
return -1;
}

11.获得指定位置的元素

#get(int index) 方法,获得指定位置的元素。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// ArrayList.java

public E get(int index) {
// 校验 index 不要超过 size
Objects.checkIndex(index, size);
// 获得 index 位置的元素
return elementData(index);
}

E elementData(int index) {
return (E) elementData[index];
}
  • 随机访问 index 位置的元素,时间复杂度为 O(1) 。

12. 设置指定位置的元素

#set(int index, E element) 方法,设置指定位置的元素。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// ArrayList.java

public E set(int index, E element) {
// 校验 index 不要超过 size
Objects.checkIndex(index, size);
// 获得 index 位置的原元素
E oldValue = elementData(index);
// 修改 index 位置为新元素
elementData[index] = element;
// 返回 index 位置的原元素
return oldValue;
}

13.转换成数组

#toArray() 方法,将 ArrayList 转换成 [] 数组。代码如下:

1
2
3
4
5
6
7
8
9
10
11
// ArrayList.java

public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}

// Arrays.java

public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}
  • 注意,返回的是 Object[] 类型噢。

实际场景下,我们可能想要指定 T 泛型的数组,那么我们就需要使用到 #toArray(T[] a) 方法。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ArrayList.java

public <T> T[] toArray(T[] a) {
// <1> 如果传入的数组小于 size 大小,则直接复制一个新数组返回
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
// <2> 将 elementData 复制到 a 中
System.arraycopy(elementData, 0, a, 0, size);
// <2.1> 如果传入的数组大于 size 大小,则将 size 赋值为 null
if (a.length > size)
a[size] = null;
// <2.2> 返回 a
return a;
}

分成 2 个情况,根据传入的 a数组是否足够大。

  • <1> 处,如果传入的数组小于 size 大小,则直接复制一个新数组返回。一般情况下,我们不会这么干。
  • <2> 处,将 elementData 复制到 a 中。
    • <2.1> 处,如果传入的数组大于 size 大小,则将 size 位置赋值为 null 。额,有点没搞懂这个有啥目的。
    • <2.2> 处,返回传入的 a

考虑到 <1> 处,可能会返回一个新数组,所以即使 <2> 返回的就是 a 数组,最好使用还是按照 a = list.toArray(a)

14.求哈希值

#hashCode() 方法,求 ArrayList 的哈希值。代码如下:

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
// ArrayList.java

public int hashCode() {
// 获得当前的数组修改次数
int expectedModCount = modCount;
// 计算哈希值
int hash = hashCodeRange(0, size);
// 如果修改次数发生改变,则抛出 ConcurrentModificationException 异常
checkForComodification(expectedModCount);
return hash;
}

int hashCodeRange(int from, int to) {
final Object[] es = elementData;
// 如果 to 超过大小,则抛出 ConcurrentModificationException 异常
if (to > es.length) {
throw new ConcurrentModificationException();
}
// 遍历每个元素,* 31 求哈希。
int hashCode = 1;
for (int i = from; i < to; i++) {
Object e = es[i];
hashCode = 31 * hashCode + (e == null ? 0 : e.hashCode());
}
return hashCode;
}

15. 判断相等

#equals(Object o) 方法,判断是否相等。代码如下:

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
// ArrayList.java

public boolean equals(Object o) {
// 如果是自己,直接返回相等
if (o == this) {
return true;
}

// 如果不为 List 类型,直接不相等
if (!(o instanceof List)) {
return false;
}

// 获得当前的数组修改次数
final int expectedModCount = modCount;
// ArrayList can be subclassed and given arbitrary behavior, but we can
// still deal with the common case where o is ArrayList precisely
// <X> 根据不同类型,调用不同比对的方法。主要考虑 ArrayList 可以直接使用其 elementData 属性,性能更优。
boolean equal = (o.getClass() == ArrayList.class)
? equalsArrayList((ArrayList<?>) o)
: equalsRange((List<?>) o, 0, size);

// 如果修改次数发生改变,则抛出 ConcurrentModificationException 异常
checkForComodification(expectedModCount);
return equal;
}
  • 为什么根据类型是否为 ArrayList ,调用了两个不同的方法去比对呢?因为普通的 List ,我们只能使用 Iterator 进行迭代,相比 ArrayList 的 elementData 属性遍历,性能会略低一些。
  • 这两个方法的代码如下,已经添加详细注释。代码如下:
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
// ArrayList.java

boolean equalsRange(List<?> other, int from, int to) {
// 如果 to 大于 es 大小,说明说明发生改变,抛出 ConcurrentModificationException 异常
final Object[] es = elementData;
if (to > es.length) {
throw new ConcurrentModificationException();
}
// 通过迭代器遍历 other ,然后逐个元素对比
var oit = other.iterator();
for (; from < to; from++) {
// 如果 oit 没有下一个,或者元素不相等,返回 false 不匹配
if (!oit.hasNext() || !Objects.equals(es[from], oit.next())) {
return false;
}
}
// 通过 oit 是否遍历完。实现大小是否相等的效果。
return !oit.hasNext();
}

private boolean equalsArrayList(ArrayList<?> other) {
// 获得 other 数组修改次数
final int otherModCount = other.modCount;
final int s = size;
boolean equal;
// 判断数组大小是否相等
if (equal = (s == other.size)) {
final Object[] otherEs = other.elementData;
final Object[] es = elementData;
// 如果 s 大于 es 或者 otherEs 的长度,说明发生改变,抛出 ConcurrentModificationException 异常
if (s > es.length || s > otherEs.length) {
throw new ConcurrentModificationException();
}
// 遍历,逐个比较每个元素是否相等
for (int i = 0; i < s; i++) {
if (!Objects.equals(es[i], otherEs[i])) {
equal = false;
break; // 如果不相等,则 break
}
}
}
// 如果 other 修改次数发生改变,则抛出 ConcurrentModificationException 异常
other.checkForComodification(otherModCount);
return equal;
}

16. 清空数组

#clear() 方法,清空数组。代码如下:

1
2
3
4
5
6
7
8
9
10
// ArrayList.java

public void clear() {
// 获得当前的数组修改次数
modCount++;
// 遍历数组,倒序设置为 null
final Object[] es = elementData;
for (int to = size, i = size = 0; i < to; i++)
es[i] = null;
}

17. 序列化数组

#writeObject(java.io.ObjectOutputStream s) 方法,实现 ArrayList 的序列化。代码如下:

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
// ArrayList.java

@java.io.Serial
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out element count, and any hidden stuff
// 获得当前的数组修改次数
int expectedModCount = modCount;

// <1> 写入非静态属性、非 transient 属性
s.defaultWriteObject();

// Write out size as capacity for behavioral compatibility with clone()
// <2> 写入 size ,主要为了与 clone 方法的兼容
s.writeInt(size);

// Write out all elements in the proper order.
// <3> 逐个写入 elementData 数组的元素
for (int i = 0; i < size; i++) {
s.writeObject(elementData[i]);
}

// 如果 other 修改次数发生改变,则抛出 ConcurrentModificationException 异常
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}

  • <1> 处,调用 ObjectOutputStream#defaultWriteObject() 方法,写入非静态属性、非 transient 属性。《Serializable原理》
  • <2> 处,写入 size ,主要为了与 clone 方法的兼容。
  • <3> 处,逐个写入 elementData 元素的数组。回过来看下 elementData 的定义,它是一个 transient 修饰的属性。为什么呢?因为 elementData 数组,并不一定是全满的,而可能是扩容的时候有一定的预留,如果直接序列化,会有很多空间的浪费,所以只序列化从 [0, size) 的元素,减少空间的占用。

18. 反序列化数组

#readObject(java.io.ObjectInputStream s) 方法,反序列化数组。代码如下:

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
// ArrayList.java

@java.io.Serial
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {

// Read in size, and any hidden stuff
// 读取非静态属性、非 transient 属性
s.defaultReadObject();

// Read in capacity
// 读取 size ,不过忽略不用
s.readInt(); // ignored

if (size > 0) {
// like clone(), allocate array based upon size not capacity
SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, size); // 不知道作甚,哈哈哈。
// 创建 elements 数组
Object[] elements = new Object[size];

// Read in all elements in the proper order.
// 逐个读取
for (int i = 0; i < size; i++) {
elements[i] = s.readObject();
}

// 赋值给 elementData
elementData = elements;
} else if (size == 0) {
// 如果 size 是 0 ,则直接使用空数组
elementData = EMPTY_ELEMENTDATA;
} else {
throw new java.io.InvalidObjectException("Invalid size: " + size);
}
}

19. 克隆

#clone() 方法,克隆 ArrayList 对象。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ArrayList.java

public Object clone() {
try {
// 调用父类,进行克隆
ArrayList<?> v = (ArrayList<?>) super.clone();
// 拷贝一个新的数组
v.elementData = Arrays.copyOf(elementData, size);
// 设置数组修改次数为 0
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
  • 注意,elementData 是重新拷贝出来的新的数组,避免和原数组共享。

20. 创建子数组

#subList(int fromIndex, int toIndex) 方法,创建 ArrayList 的子数组。代码如下:

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
// ArrayList.java

public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList<>(this, fromIndex, toIndex);
}

private static class SubList<E> extends AbstractList<E> implements RandomAccess {

/**
* 根 ArrayList
*/
private final ArrayList<E> root;
/**
* 父 SubList
*/
private final SubList<E> parent;
/**
* 起始位置
*/
private final int offset;
/**
* 大小
*/
private int size;

// ... 省略代码
}
  • 实际使用时,一定要注意,SubList 不是一个只读数组,而是和根数组 root 共享相同的 elementData 数组,只是说限制了 [fromIndex, toIndex) 的范围。

21. 创建 Iterator 迭代器

#iterator() 方法,创建迭代器。一般情况下,我们使用迭代器遍历 ArrayList、LinkedList 等等 List 的实现类。代码如下:

1
2
3
4
5
// ArrayList.java

public Iterator<E> iterator() {
return new Itr();
}
  • 创建 Itr 迭代器。Itr 实现 java.util.Iterator 接口,是 ArrayList 的内部类。虽然说 AbstractList 也提供了一个 Itr 的实现,但是 ArrayList 为了更好的性能,所以自己实现了,在其类上也有注释“An optimized version of AbstractList.Itr”。

Itr 一共有 3 个属性,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ArrayList.java#Itr

/**
* 下一个访问元素的位置,从下标 0 开始。
*/
int cursor; // index of next element to return
/**
* 上一次访问元素的位置。
*
* 1. 初始化为 -1 ,表示无上一个访问的元素
* 2. 遍历到下一个元素时,lastRet 会指向当前元素,而 cursor 会指向下一个元素。这样,如果我们要实现 remove 方法,移除当前元素,就可以实现了。
* 3. 移除元素时,设置为 -1 ,表示最后访问的元素不存在了,都被移除咧。
*/
int lastRet = -1; // index of last element returned; -1 if no such
/**
* 创建迭代器时,数组修改次数。
*
* 在迭代过程中,如果数组发生了变化,会抛出 ConcurrentModificationException 异常。
*/
int expectedModCount = modCount;

// prevent creating a synthetic constructor
Itr() {}

下面,来看看 Itr 对 Iterator 的 4 个实现方法。

1.#hasNext() 方法,判断是否还可以继续迭代。代码如下:

1
2
3
4
5
// ArrayList.java#Itr

public boolean hasNext() {
return cursor != size;
}

cursor 如果等于 size ,说明已经到数组末尾,无法继续迭代了

2.#next() 方法,返回当前元素。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ArrayList.java#Itr

public E next() {
// 校验是否数组发生了变化
checkForComodification();
// 判断如果超过 size 范围,抛出 NoSuchElementException 异常
int i = cursor; // <1> i 记录当前 cursor 的位置
if (i >= size)
throw new NoSuchElementException();
// 判断如果超过 elementData 大小,说明可能被修改了,抛出 ConcurrentModificationException 异常
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
// <2> cursor 指向下一个位置
cursor = i + 1;
// <3> 返回当前位置的元素
return (E) elementData[lastRet = i]; // <4> 此处,会将 lastRet 指向当前位置
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
  • <1> 处,记录当前 cursor 的位置。因为我们当前返回的就是要求 cursor 位置的元素。
  • <2> 处,cursor 指向下一个位置。
  • <3> 处,返回当前位置的元素。同时在 <4> 处,会将 lastRet 指向当前位置。

3.#remove() 方法,移除当前元素。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ArrayList.java#Itr

public void remove() {
// 如果 lastRet 小于 0 ,说明没有指向任何元素,抛出 IllegalStateException 异常
if (lastRet < 0)
throw new IllegalStateException();
// 校验是否数组发生了变化
checkForComodification();

try {
// <1> 移除 lastRet 位置的元素
ArrayList.this.remove(lastRet);
// <2> cursor 指向 lastRet 位置,因为被移了,所以需要后退下
cursor = lastRet;
// <3> lastRet 标记为 -1 ,因为当前元素被移除了
lastRet = -1;
// <4> 记录新的数组的修改次数
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
  • <1> 处,调用 #remove(int index) 方法,移除 lastRet 位置的元素。所以要注意,如果移除元素比较前面,会将后面位置的往前挪,即复制,可能比较消耗性能。
  • <2> 处,cursor 指向 lastRet 位置,因为被移了,所以需要后退下。
  • <3> 处,lastRet 标记为 -1 ,因为当前元素被移除了。
  • <4> 处,记录新的数组的修改次数。因为此处修改了数组,如果不修改下,后续迭代肯定会报错。

4.#forEachRemaining(Consumer<? super E> action) 方法,消费剩余未迭代的元素。代码如下:

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
// ArrayList.java#Itr

@Override
public void forEachRemaining(Consumer<? super E> action) {
// 要求 action 非空
Objects.requireNonNull(action);
// 获得当前数组大小
final int size = ArrayList.this.size;
// 记录 i 指向 cursor
int i = cursor;
if (i < size) {
// 判断如果超过 elementData 大小,说明可能被修改了,抛出 ConcurrentModificationException 异常
final Object[] es = elementData;
if (i >= es.length)
throw new ConcurrentModificationException();
// 逐个处理
for (; i < size && modCount == expectedModCount; i++)
action.accept(elementAt(es, i));
// update once at end to reduce heap write traffic
// 更新 cursor 和 lastRet 的指向
cursor = i;
lastRet = i - 1;
// 校验是否数组发生了变化
checkForComodification();
}
}

22.创建 ListIterator 迭代器

#listIterator(...) 方法,创建 ListIterator 迭代器。代码如下:

1
2
3
4
5
6
7
8
9
10
// ArrayList.java

public ListIterator<E> listIterator(int index) {
rangeCheckForAdd(index);
return new ListItr(index);
}

public ListIterator<E> listIterator() {
return new ListItr(0);
}
  • 创建 ListItr 迭代器。ListItr 实现 java.util.ListIterator 接口,是 ArrayList 的内部类。虽然说 AbstractList 也提供了一个 ListItr 的实现,但是 ArrayList 为了更好的性能,所以自己实现了,在其类上也有注释“An optimized version of AbstractList.ListItr”。

ListItr 直接继承 Itr 类,无自定义的属性。代码如下:

1
2
3
4
5
6
// ArrayList.java#ListItr

ListItr(int index) {
super();
cursor = index;
}

可以手动设置指定的位置开始迭代。

实现代码:

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
// ArrayList.java#ListItr

/**
* @return 是否有前一个
*/
public boolean hasPrevious() {
return cursor != 0;
}

/**
* @return 下一个位置
*/
public int nextIndex() {
return cursor;
}

/**
* @return 前一个位置
*/
public int previousIndex() {
return cursor - 1;
}

/**
* @return 前一个元素
*/
@SuppressWarnings("unchecked")
public E previous() {
// 校验是否数组发生了变化
checkForComodification();
// 判断如果小于 0 ,抛出 NoSuchElementException 异常
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
// 判断如果超过 elementData 大小,说明可能被修改了,抛出 ConcurrentModificationException 异常
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
// cursor 指向上一个位置
cursor = i;
// 返回当前位置的元素
return (E) elementData[lastRet = i]; // 此处,会将 lastRet 指向当前位置
}

/**
* 设置当前元素
*
* @param e 设置的元素
*/
public void set(E e) {
// 如果 lastRet 无指向,抛出 IllegalStateException 异常
if (lastRet < 0)
throw new IllegalStateException();
// 校验是否数组发生了变化
checkForComodification();

try {
// 设置
ArrayList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

/**
* 添加元素当当前位置
*
* @param e 添加的元素
*/
public void add(E e) {
// 校验是否数组发生了变化
checkForComodification();

try {
// 添加元素到当前位置
int i = cursor;
ArrayList.this.add(i, e);
// cursor 指向下一个位置,因为当前位置添加了新的元素,所以需要后挪
cursor = i + 1;
// lastRet 标记为 -1 ,因为当前元素并未访问
lastRet = -1;
// 记录新的数组的修改次数
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

23.分割迭代器

(Spliterator)分割迭代器,是java1.8新提出的能够进行并行遍历的迭代器.用于遍历和分区源元素的对象。Spliterator覆盖的元素源可以是数组, 集合Collection,IO通道或生成器函数。

Spliterator可以单独遍历元素(tryAdvance())或批量顺序遍历元素(forEachRemaining())。

代码如下:

1
2
3
4
5
//ArrayList.java  
@Override
public Spliterator<E> spliterator() {
return new ArrayListSpliterator<>(this, 0, -1, 0);
}

内部类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static final class ArrayListSpliterator<E> implements Spliterator<E> {
private final ArrayList<E> list;
private int index; // current index, modified on advance/split 存储起始下标;
private int fence; // -1 until used; then one past last index 结束下标;
private int expectedModCount; // initialized when fence set 修改次数modCount;

/** Create new spliterator covering the given range */
ArrayListSpliterator(ArrayList<E> list, int origin, int fence,
int expectedModCount) {
this.list = list; // OK if null unless traversed
this.index = origin;
this.fence = fence;
this.expectedModCount = expectedModCount;
}
.....省略方法

getFence方法

获取fence结束下标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private int getFence() { // initialize fence to size on first use
int hi; // (a specialized variant appears in method forEach)
ArrayList<E> lst;
//小于0,说明赋值过了,直接返回
if ((hi = fence) < 0) {
//空集合,直接赋值0
if ((lst = list) == null)
hi = fence = 0;
else {
//记录修改次数
expectedModCount = lst.modCount;
//将集合大小赋值给结束下标
hi = fence = lst.size;
}
}
//返回
return hi;
}

trySplit方法

分割list,返回一个新分割的Spilterator实例;相当于二分法,该方法会递归。

>>>: 无符号右移运算符,num >>> 1,相当于num除以2

1
2
3
4
5
6
7
8
9
public ArrayListSpliterator<E> trySplit() {
//hi结束下标,lo开始下标,mid:中间位置
int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;
//lo >= mid时,表示不能再分割,返回null
//lo < mid时,表示可以分割,切割(lo,mid)出去,相当于取list前半部分返回,同时更新index=mid
return (lo >= mid) ? null : // divide range in half unless too small
new ArrayListSpliterator<E>(list, lo, index = mid,
expectedModCount);
}

tryAdvance方法

根据action对list 当前元素进行函数式操作,如果存在剩余元素,则对其执行给定操作,返回true;否则返回false。如果此Spliterator为ORDERED,则会对遇见顺序中的下一个元素执行操作。操作抛出的异常会转发给调用者。

如果指定的操作为null则会抛出NullPointerException。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public boolean tryAdvance(Consumer<? super E> action) {
//操作为空,抛异常
if (action == null)
throw new NullPointerException();
//获取起始下标和结束下标
int hi = getFence(), i = index;

if (i < hi) {
//起始下标指向下一个元素
index = i + 1;
@SuppressWarnings("unchecked") E e = (E)list.elementData[i];
//执行操作
action.accept(e);
if (list.modCount != expectedModCount)
throw new ConcurrentModificationException();
return true;
}
//起始下标大于结束下标直接返回false
return false;
}

forEachRemaining方法

对list所有元素进行函数式操作。在当前线程中按顺序对每个剩余元素执行给定操作,直到所有元素都已处理或操作抛出异常为止。如果此Spliterator为ORDERED,则会按遇见顺序执行操作。操作抛出的异常会转发给调用者。

说明: 默认实现重复调用tryAdvance(java.util.function.Consumer <? super T>),直到它返回false。应该尽可能地覆盖它。

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
public void forEachRemaining(Consumer<? super E> action) {
int i, hi, mc; // hoist accesses and checks from loop
ArrayList<E> lst; Object[] a;
if (action == null)
throw new NullPointerException();
//集合不为null,并且集合中数组不为空
if ((lst = list) != null && (a = lst.elementData) != null) {
//结束下标小于0,将修改次数赋值给mc,集合大小赋值给hi
if ((hi = fence) < 0) {
mc = lst.modCount;
hi = lst.size;
}
else
mc = expectedModCount;
//起始下标大于等于0,结束下标小于等于数组长度
if ((i = index) >= 0 && (index = hi) <= a.length) {
//遍历元素进行指定操作
for (; i < hi; ++i) {
@SuppressWarnings("unchecked") E e = (E) a[i];
action.accept(e);
}
if (lst.modCount == mc)
return;
}
}
throw new ConcurrentModificationException();
}

estimateSize方法

返回长度,估算大小,如果无限,未知或计算成本太高,则返回Long.MAX_VALUE

1
2
3
public long estimateSize() {
return (long) (getFence() - index);
}

characteristics方法

返回特征值:ORDERED、SIZED、SUBSIZED。

ORDERED = 0x00000010

SIZED = 0x00000040;

SUBSIZED = 0x00004000

1
2
3
4
public int characteristics() {
return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
}

使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SpliteratorTest {

public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 100 ; i++) {
list.add(i+"");
}

// 分割迭代器
Spliterator spliterator = list.spliterator();
Spliterator s1 = spliterator.trySplit();
Spliterator s2 = spliterator.trySplit();

System.out.println("===============spliterator================");
spliterator.forEachRemaining((i) -> System.out.print(i+" "));
System.out.println();
System.out.println("===============s1================");
s1.forEachRemaining((i) -> System.out.print(i+" "));
System.out.println();
System.out.println("===============s2================");
s2.forEachRemaining((i) -> System.out.print(i+" "));
System.out.println();
}
}

运行结果:

image-20230401171532104

24.removeIf方法

removeIf() 方法用于删除所有满足特定条件的数组元素。

removeIf() 方法的语法为:arraylist.removeIf(Predicate<E> filter)

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
public boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
// figure out which elements are to be removed
// any exception thrown from the filter predicate at this stage
// will leave the collection unmodified
int removeCount = 0;
//记录被删除元素的下标
final BitSet removeSet = new BitSet(size);
//修改次数
final int expectedModCount = modCount;
//集合大小
final int size = this.size;
//遍历把符合条件的待删除的元素下标设置到BitSet中,即BitSet中对应的下标设为true.
for (int i=0; modCount == expectedModCount && i < size; i++) {
@SuppressWarnings("unchecked")
final E element = (E) elementData[i];
if (filter.test(element)) {
removeSet.set(i);
removeCount++;
}
}
//检查是否有其他线程并发修改该ArrayList
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}

// shift surviving elements left over the spaces left by removed elements
final boolean anyToRemove = removeCount > 0;
//符合条件的元素>0
if (anyToRemove) {
//新的集合大小
final int newSize = size - removeCount;
//移除相应元素
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
//通过nextClearBit(i) 为返回下一个false位置,即表示下标i是无需移除的元素
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
//newSize之后的元素置空
for (int k=newSize; k < size; k++) {
elementData[k] = null; // Let gc do its work
}
this.size = newSize;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}

return anyToRemove;
}

25.replaceAll方法

根据UnaryOperator对象操作list(函数式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void replaceAll(UnaryOperator<E> operator) {
//判断operator是否为空
Objects.requireNonNull(operator);
//
final int expectedModCount = modCount;
final int size = this.size;
//循环条件expectedModCount==modCount&& i<size,防止并发的时候,modCount被其他线程修改
for (int i=0; modCount == expectedModCount && i < size; i++) {
//执行操作
elementData[i] = operator.apply((E) elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}

26.sort方法

根据比较器对list进行排序

1
2
3
4
5
6
7
8
public void sort(Comparator<? super E> c) {
final int expectedModCount = modCount;
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}

27.forEach方法

对list进行函数式循环(重写Iterable接口中的方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
//修改次数
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
//执行操作
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}

小结

  • ArrayList 是基于 [] 数组实现的 List 实现类,支持在数组容量不够时,一般按照 1.5自动扩容。同时,它支持手动扩容、手动缩容。

  • ArrayList 随机访问时间复杂度是 O(1) ,查找指定元素的平均时间复杂度是 O(n)

    时间复杂度的计算方式可以看看《算法复杂度分析(上):分析算法运行时,时间资源及空间资源的消耗》《算法复杂度分析(下):最好、最坏、平均、均摊等时间复杂度概述》 两文。

  • ArrayList 移除指定位置的元素的最好时间复杂度是 O(1) ,最坏时间复杂度是 O(n) ,平均时间复杂度是 O(n) 。

    最好时间复杂度发生在末尾移除的情况。

  • ArrayList 移除指定元素的时间复杂度是 O(n) 。

    因为首先需要进行查询,然后在使用移除指定位置的元素,无论怎么计算,都需要 O(n) 的时间复杂度。

  • ArrayList 添加元素的最好时间复杂度是 O(1) ,最坏时间复杂度是 O(n) ,平均时间复杂度是 O(n) 。

    最好时间复杂度发生在末尾添加的情况。

Redis String 的数据结构,实现方式是类似 Java ArrayList 的方式

链表 LinkedList

LinkedList ,基于节点实现的双向链表的 List ,每个节点都指向前一个和后一个节点从而形成链表。

1.类图

image-20230403084549169

下面 3 个接口是 ArrayList 一致的:

下面 1 个接口是少于 ArrayList 的:

  • java.util.RandomAccess 接口,LinkedList 不同于 ArrayList 的很大一点,==不支持随机访问==。

如下1 个接口是多于 ArrayList 的:

  • java.util.Deque 接口,提供双端队列的功能,LinkedList 支持快速的在头尾添加元素和读取元素,所以很容易实现该特性。

因为LinkedList 实现了 Deque 接口,添加或者移除一个元素会有多种方法;可以作为队列使用,也可以作为栈使用,还可以作为双端队列。

2.属性

image-20230403085351937

  • 通过 Node 节点指向前后节点,从而形成双向链表。
  • firstlast属性:链表的头尾指针。
    • 在初始时候,firstlast 指向 null ,因为此时暂时没有 Node 节点。
    • 在添加完首个节点后,创建对应的 Node 节点 node1 ,前后指向 null 。此时,firstlast 指向该 Node 节点。
    • 继续添加一个节点后,创建对应的 Node 节点 node2 ,其 prev = node1next = null ,而 node1prev = nullnext = node2 。此时,first 保持不变,指向 node1last 发生改变,指向 node2
  • size 属性:链表的节点数量。通过它进行计数,避免每次需要 List 大小时,从头到尾的遍历
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
// LinkedList.java

/**
* 链表大小
*/
transient int size = 0;

/**
* 头节点
*
* Pointer to first node.
*/
transient Node<E> first;

/**
* 尾节点
*
* Pointer to last node.
*/
transient Node<E> last;

/**
* 节点
*
* @param <E> 元素泛型
*/
private static class Node<E> {

/**
* 元素
*/
E item;
/**
* 前一个节点
*/
Node<E> next;
/**
* 后一个节点
*/
Node<E> prev;

Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}

}

3.构造方法

LinkedList 一共有两个构造方法

1
2
3
4
5
6
7
public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}

4. 添加单个元素

1.添加单个元素到末尾

#add(E e) 方法,顺序添加单个元素到链表。代码如下:

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
// LinkedList.java

public boolean add(E e) {
// <X> 添加末尾
linkLast(e);
return true;
}

void linkLast(E e) {
// <1> 记录原 last 节点
final Node<E> l = last;
// <2> 创建新节点
// 第一个参数表示,newNode 的前一个节点为 l 。
// 第二个参数表示,e 为元素。
// 第三个参数表示,newNode 的后一个节点为 null 。
final Node<E> newNode = new Node<>(l, e, null);
// <3> last 指向新节点
last = newNode;
// <4.1> 如果原 last 为 null ,说明 first 也为空,则 first 也指向新节点
if (l == null)
first = newNode;
// <4.2> 如果原 last 非 null ,说明 first 也非空,则原 last 的 next 指向新节点。
else
l.next = newNode;
// <5> 增加链表大小
size++;
// <6> 增加数组修改次数
modCount++;
}
  • <X> 处,调用 #linkLast(E e) 方法,将新元素添加到链表的尾巴。所以,#add(E e) 方法,实际就是 #linkLast(E e) 方法。
  • 总体来说,代码实现比较简单。重点就是对 last 的处理。
  • 相比 ArrayList 来说,无需考虑容量不够时的扩容。

2.添加元素到指定位置

看看 #add(int index, E element) 方法,插入单个元素到指定位置。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// LinkedList.java

public void add(int index, E element) {
// 校验不要超过范围
checkPositionIndex(index);

// <1> 如果刚好等于链表大小,直接添加到尾部即可
if (index == size)
linkLast(element);
// <2> 添加到第 index 的节点的前面
else
linkBefore(element, node(index));
}
  • <1> 处,如果刚好等于链表大小,直接调用 #linkLast(E element) 方法,添加到尾部即可。
  • <2> 处,先调用 #node(int index) 方法,获得第 index 位置的 Node 节点 node 。然后,调用 #linkBefore(E element, Node node) 方法,将新节点添加到 node 的前面。相当于说,node 的前一个节点的 next 指向新节点,nodeprev 指向新节点。

#node(int index) 方法,获得第 index 个 Node 节点。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// LinkedList.java

Node<E> node(int index) {
// assert isElementIndex(index);

// 如果 index 小于 size 的一半,就正序遍历,获得第 index 个节点
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
// 如果 index 大于 size 的一半,就倒序遍历,获得第 index 个节点
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}

#linkBefore(E e, Node<E> succ) 方法,添加元素 esucc 节点的前面。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// LinkedList.java

void linkBefore(E e, Node<E> succ) {
// assert succ != null;
// 获得 succ 的前一个节点
final Node<E> pred = succ.prev;
// 创建新的节点 newNode
final Node<E> newNode = new Node<>(pred, e, succ);
// <Y> 设置 succ 的前一个节点为新节点
succ.prev = newNode;
// 如果 pred 为 null ,说明 first 也为空,则 first 也指向新节点
if (pred == null)
first = newNode;
// 如果 pred 非 null ,说明 first 也为空,则 pred 也指向新节点
else
pred.next = newNode;
// 增加链表大小
size++;
// 增加数组修改次数
modCount++;
}

3.添加元素到链表头或尾

因为 LinkedList 实现了 Deque 接口,所以它实现了 #addFirst(E e)#addLast(E e) 方法,分别添加元素到链表的头尾。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// LinkedList.java 实现 Deque 接口

public void addFirst(E e) {
linkFirst(e);
}
public boolean offerFirst(E e) {
addFirst(e); // 调用上面的方法
return true;
}

public void addLast(E e) {
linkLast(e);
}
public boolean offerLast(E e) {
addLast(e); // 调用上面的方法
return true;
}
  • #linkLast(E e) 方法,和 #add(E e) 方法是一致的。
  • #addFirst(E e) 方法,调用 #linkFirst(E e) 方法,添加元素到队头。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// LinkedList.java

private void linkFirst(E e) {
// 记录原 first 节点
final Node<E> f = first;
// 创建新节点
final Node<E> newNode = new Node<>(null, e, f);
// first 指向新节点
first = newNode;
// 如果原 first 为空,说明 last 也为空,则 last 也指向新节点
if (f == null)
last = newNode;
// 如果原 first 非空,说明 last 也非空,则原 first 的 next 指向新节点。
else
f.prev = newNode;
// 增加链表大小
size++;
// 增加数组修改次数
modCount++;
}

因为 LinkedList 实现了 Queue 接口,所以它实现了 #push(E e)#offer(E e) 方法,添加元素到链表的头尾。代码如下:

1
2
3
4
5
6
7
8
9
// LinkedList.java 实现 Queue 接口

public void push(E e) {
addFirst(e);
}

public boolean offer(E e) {
return add(e);
}

总的来说,添加单个元素,分成三个情况:

  • 添加元素到队头
  • 添加元素到队尾
  • 添加元素到中间

5.链表扩容

LinkedList 不存在扩容的需求,因为通过 Node 的前后指向即可。

6.添加多个元素

#addAll(Collection<? extends E> c) 方法,批量添加多个元素。代码如下:

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
// LinkedList.java

public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}

public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);

// <1> 将 c 转成 a 数组
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0) // 如果无添加元素,直接返回 false 数组未变更
return false;

// <2> 获得第 index 位置的节点 succ ,和其前一个节点 pred
Node<E> pred, succ;
if (index == size) { // 如果 index 就是链表大小,那说明插入队尾,所以 succ 为 null ,pred 为 last 。
succ = null;
pred = last;
} else { // 如果 index 小于链表大小,则 succ 是第 index 个节点,prev 是 succ 的前一个二节点。
succ = node(index);
pred = succ.prev;
}

// <3> 遍历 a 数组,添加到 pred 的后面
for (Object o : a) {
// 创建新节点
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
// 如果 pred 为 null ,说明 first 也为 null ,则直接将 first 指向新节点
if (pred == null)
first = newNode;
// pred 下一个指向新节点
else
pred.next = newNode;
// 修改 pred 指向新节点
pred = newNode;
}

// <4> 修改 succ 和 pred 的指向
if (succ == null) { // 如果 succ 为 null ,说明插入队尾,则直接修改 last 指向最后一个 pred
last = pred;
} else { // 如果 succ 非 null ,说明插入到 succ 的前面
pred.next = succ; // prev 下一个指向 succ
succ.prev = pred; // succes 前一个指向 pred
}

// <5> 增加链表大小
size += numNew;
// <6> 增加数组修改次数
modCount++;
// 返回 true 数组有变更
return true;
}
  • #addAll(Collection<? extends E> c) 方法,其内部调用的是 #addAll(int index, Collection<? extends E> c) 方法,表示在队列之后,继续添加 c 集合。
  • <2> 处,获得第 index 位置的节点 succ ,和其前一个节点 pred 。分成两种情况,实际上,ArrayList 在添加 c 集合的时候,也是分成跟 LinkedList 一样的两种情况,只是说 LinkedList 在一个方法统一实现了。
  • <3> 处,遍历 a 数组,添加到 pred 的后面。其实,我们可以把 pred 理解成“尾巴”,然后不断的指向新节点,而新节点又称为新的 pred 尾巴。如此反复插入~
  • <4> 处,修改 succpred 的指向。根据 <2> 处分的两种情况,进行处理。

7.移除单个元素

1.移除指定位置元素

#remove(int index) 方法,移除指定位置的元素,并返回该位置的原元素。代码如下:

1
2
3
4
5
6
7
// LinkedList.java

public E remove(int index) {
checkElementIndex(index);
// 获得第 index 的 Node 节点,然后进行移除。
return unlink(node(index));
}
  • 首先,调用 #node(int index) 方法,获得第 index 的 Node 节点。然后偶,调用 #unlink(Node<E> x) 方法,移除该节点。
  • #unlink(Node<E> x) 方法,代码如下:
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
// LinkedList.java

E unlink(Node<E> x) {
// assert x != null;
// <1> 获得 x 的前后节点 prev、next
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;

// <2> 将 prev 的 next 指向下一个节点
if (prev == null) { // <2.1> 如果 prev 为空,说明 first 被移除,则直接将 first 指向 next
first = next;
} else { // <2.2> 如果 prev 非空
prev.next = next; // prev 的 next 指向 next
x.prev = null; // x 的 pre 指向 null
}

// <3> 将 next 的 prev 指向上一个节点
if (next == null) { // <3.1> 如果 next 为空,说明 last 被移除,则直接将 last 指向 prev
last = prev;
} else { // <3.2> 如果 next 非空
next.prev = prev; // next 的 prev 指向 prev
x.next = null; // x 的 next 指向 null
}

// <4> 将 x 的 item 设置为 null ,帮助 GC
x.item = null;
// <5> 减少链表大小
size--;
// <6> 增加数组的修改次数
modCount++;
return element;
}
  • <2> 处,将 prevnext 指向下一个节点。其中,<2.1> 处,是移除队头 first 的情况。
  • <3> 处,将 nextprev 指向上一个节点。其中,<3.1> 处,如果 next 为空,说明队尾 last 被移除的情况。

2.移除首个元素

#remove(Object o) 方法,移除首个为 o 的元素,并返回是否移除到。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// LinkedList.java

public boolean remove(Object o) {
if (o == null) { // o 为 null 的情况
// 顺序遍历,找到 null 的元素后,进行移除
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
// 顺序遍历,找到等于 o 的元素后,进行移除
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
  • 相比 #remove(int index) 方法来说,需要去寻找首个等于 o 的节点进行移除。当然,最终还是调用 #unlink(Node<E> x) 方法,移除该节点。

3.移除首个、末尾元素

#removeFirstOccurrence(Object o)#removeLastOccurrence(Object o) 方法,分别实现移除链表首个节点和最后节点。代码如下:

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
// LinkedList.java 实现 Deque 接口

public boolean removeFirstOccurrence(Object o) { // 移除首个
return remove(o);
}

public boolean removeLastOccurrence(Object o) {
if (o == null) { // o 为 null 的情况
// 倒序遍历,找到 null 的元素后,进行移除
for (Node<E> x = last; x != null; x = x.prev) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
// 倒序遍历,找到等于 o 的元素后,进行移除
for (Node<E> x = last; x != null; x = x.prev) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}

#remove() 方法,移除链表首个节点。代码如下:

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
// LinkedList.java 实现 Queue 接口

public E remove() {
return removeFirst();
}

public E removeFirst() {
final Node<E> f = first;
// <1> 如果链表为空,抛出 NoSuchElementException 异常
if (f == null)
throw new NoSuchElementException();
// <2> 移除链表时首个元素
return unlinkFirst(f);
}

private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
// 获得 f 的下一个节点
final Node<E> next = f.next;
// 设置 f 的 item 为 null ,帮助 GC
f.item = null;
// 设置 f 的 next 为 null ,帮助 GC
f.next = null; // help GC
// 修改 fisrt 指向 next
first = next;
// 修改 next 节点的 prev 指向 null
if (next == null) // 如果链表只有一个元素,说明被移除后,队列就是空的,则 last 设置为 null
last = null;
else
next.prev = null;
// 链表大小减一
size--;
// 增加数组修改次数
modCount++;
return element;
}
  • <1> 处,如果链表为空,抛出 NoSuchElementException 异常。
  • <2> 处,移除链表时首个元素。因为 LinkedList 有 firstlast 头尾节点,所以添加和删除操作,都可能需要小心处理。

4.移除末尾节点

#removeLast() 方法,移除链表最后一个节点。代码如下:

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
// LinkedList.java 实现 Deque 接口

public E removeLast() {
final Node<E> l = last;
// 如果链表为空,则抛出 NoSuchElementException 移除
if (l == null)
throw new NoSuchElementException();
// 移除链表的最后一个元素
return unlinkLast(l);
}

private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
// 获得 f 的上一个节点
final Node<E> prev = l.prev;
// 设置 l 的 item 为 null ,帮助 GC
l.item = null;
// 设置 l 的 prev 为 null ,帮助 GC
l.prev = null; // help GC
// 修改 last 指向 prev
last = prev;
// 修改 prev 节点的 next 指向 null
if (prev == null) // 如果链表只有一个元素,说明被移除后,队列就是空的,则 first 设置为 null
first = null;
else
prev.next = null;
// 链表大小减一
size--;
// 增加数组修改次数
modCount++;
return element;
}

5.移除链表的头或尾

#poll() 方法,移除链表的头或尾,差异点在于链表为空时候,不会抛出 NoSuchElementException 异常。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// LinkedList.java 实现 Queue 接口

public E poll() { // 移除头
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}

public E pop() {
return removeFirst(); // 这个方法,如果队列为空,还是会抛出 NoSuchElementException 异常。
}

// LinkedList.java 实现 Deque 接口

public E pollFirst() { // 移除头
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}

public E pollLast() { // 移除尾
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}

8.移除多个元素

#removeAll(Collection<?> c) 方法,批量移除指定的多个元素。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// AbstractCollection.java

public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
// 获得迭代器
Iterator<?> it = iterator();
// 通过迭代器遍历
while (it.hasNext()) {
// 如果 c 中存在该元素,则进行移除
if (c.contains(it.next())) {
it.remove();
modified = true; // 标记修改
}
}
return modified;
}
  • 该方法,是通过父类 AbstractCollection 来实现的,通过迭代器来遍历 LinkedList ,然后判断 c 中如果包含,则进行移除。

#retainAll(Collection<?> c) 方法,求 LinkedList 和指定多个元素的交集。简单来说,恰好和 #removeAll(Collection<?> c) 相反,移除不在 c 中的元素。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// AbstractCollection.java

public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
// 获得迭代器
Iterator<E> it = iterator();
// 通过迭代器遍历
while (it.hasNext()) {
// <X> 如果 c 中不存在该元素,则进行移除
if (!c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}

9.查找单个元素

#indexOf(Object o) 方法,查找首个为指定元素的位置。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// LinkedList.java

public int indexOf(Object o) {
int index = 0;
if (o == null) { // 如果 o 为 null 的情况
// 顺序遍历,如果 item 为 null 的节点,进行返回
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index; // 找到
index++;
}
} else { // 如果 o 非 null 的情况
// 顺序遍历,如果 item 为 o 的节点,进行返回
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index; // 找到
index++;
}
}
// 未找到
return -1;
}

#contains(Object o) 方法,就是基于该方法实现。代码如下:

1
2
3
4
5
// LinkedList.java

public boolean contains(Object o) {
return indexOf(o) >= 0;
}

有时我们需要查找最后一个为指定元素的位置,所以会使用到 #lastIndexOf(Object o) 方法。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// LinkedList.java

public int lastIndexOf(Object o) {
int index = size;
if (o == null) { // 如果 o 为 null 的情况
// 倒序遍历,如果 item 为 null 的节点,进行返回
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (x.item == null)
return index; // 找到
}
} else { // 如果 o 非 null 的情况
// 倒序遍历,如果 item 为 o 的节点,进行返回
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (o.equals(x.item))
return index; // 找到
}
}
// 未找到
return -1;
}

10.获得指定位置的元素

1.获取指定位置元素

#get(int index) 方法,获得指定位置的元素。代码如下:

1
2
3
4
5
6
7
// LinkedList.java

public E get(int index) {
checkElementIndex(index);
// 基于 node(int index) 方法实现
return node(index).item;
}
  • 随机访问 index 位置的元素,时间复杂度为 O(n) 。

2.获取头尾元素

因为 LinkedList 实现了 Deque 接口,所以它实现了 #peekFirst()#peekLast() 方法,分别获得元素到链表的头尾。代码如下:

1
2
3
4
5
6
7
8
9
10
11
// LinkedList.java 实现 Deque 接口

public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}

public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}

3.获取头元素

因为 LinkedList 实现了 Queue 接口,所以它实现了 #peek()#element() 方法,分别获得元素到链表的头。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// LinkedList.java 实现 Queue 接口

public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}

public E element() { // 如果链表为空识,抛出 NoSuchElementException 异常
return getFirst();
}
public E getFirst() {
final Node<E> f = first;
if (f == null) // 如果链表为空识,抛出 NoSuchElementException 异常
throw new NoSuchElementException();
return f.item;
}

11.设置指定位置的元素

#set(int index, E element) 方法,设置指定位置的元素。代码如下:

1
2
3
4
5
6
7
8
9
10
11
// LinkedList.java

public E set(int index, E element) {
checkElementIndex(index);
// 获得第 index 位置的节点
Node<E> x = node(index);
E oldVal = x.item;
// 修改对应的值
x.item = element;
return oldVal;
}

12.转换成数组

1.object类型数组

#toArray() 方法,将LinkedList 转换成 [] 数组。代码如下:

1
2
3
4
5
6
7
8
9
10
11
// LinkedList.java

public Object[] toArray() {
// 创建 Object 数组
Object[] result = new Object[size];
// 顺序遍历节点,设置到 Object 数组中
int i = 0;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
return result;
}

2.指定类型数组

实际场景下,我们可能想要指定 T 泛型的数组,那么我们就需要使用到 #toArray(T[] a) 方法。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// LinkedList.java

public <T> T[] toArray(T[] a) {
// <1> 如果传入的数组小于 size 大小,则直接复制一个新数组返回
if (a.length < size)
a = (T[])java.lang.reflect.Array.newInstance(
a.getClass().getComponentType(), size);
// <2> 顺序遍历链表,复制到 a 中
int i = 0;
Object[] result = a;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;

// <2.1> 如果传入的数组大于 size 大小,则将 size 赋值为 null
if (a.length > size)
a[size] = null;

// <2.2> 返回 a
return a;
}

13.求哈希值

#hashCode() 方法,求 LinkedList 的哈希值。代码如下:

1
2
3
4
5
6
7
8
9
// AbstractList.java

public int hashCode() {
int hashCode = 1;
// 遍历,求哈希
for (E e : this)
hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
return hashCode;
}
  • 该方法,是通过父类 AbstractList 来实现的,通过 for 来遍历 LinkedList ,然后进行求哈希。它最终会编译转换成 Iterator 迭代器。

14.判断相等

#equals(Object o) 方法,判断是否相等。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// AbstractList.java

public boolean equals(Object o) {
// 如果 o 就是自己,直接返回 true
if (o == this)
return true;
// 如果不为 List 类型,直接返回 false
if (!(o instanceof List))
return false;

// 创建迭代器,顺序遍历比对
ListIterator<E> e1 = listIterator();
ListIterator<?> e2 = ((List<?>) o).listIterator();
while (e1.hasNext() && e2.hasNext()) {
E o1 = e1.next();
Object o2 = e2.next();
if (!(o1==null ? o2==null : o1.equals(o2))) // 如果不相等,返回 false
return false;
}
// 如果有迭代器没有遍历完,说明两者长度不等,所以就不相等;否则,就相等了
return !(e1.hasNext() || e2.hasNext());
}
  • 该方法,是通过父类 AbstractList 来实现的,通过迭代器,实现遍历比对。

15.清空链表

#clear() 方法,清空链表。代码如下:

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
// LinkedList.java

public void clear() {
// Clearing all of the links between nodes is "unnecessary", but:
// - helps a generational GC if the discarded nodes inhabit
// more than one generation
// - is sure to free memory even if there is a reachable Iterator
// 顺序遍历链表,设置每个节点前后指向为 null
// 通过这样的方式,帮助 GC
for (Node<E> x = first; x != null; ) {
// 获得下一个节点
Node<E> next = x.next;
// 设置 x 的 item、next、prev 为空。
x.item = null;
x.next = null;
x.prev = null;
// 设置 x 为下一个节点
x = next;
}
// 清空 first 和 last 指向
first = last = null;
// 设置链表大小为 0
size = 0;
// 增加数组修改次数
modCount++;
}

16.序列化链表

#writeObject(java.io.ObjectOutputStream s) 方法,实现 LinkedList 的序列化。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// LinkedList.java

@java.io.Serial
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
// 写入非静态属性、非 transient 属性
s.defaultWriteObject();

// Write out size
// 写入链表大小
s.writeInt(size);

// Write out all elements in the proper order.
// 顺序遍历,逐个序列化
for (Node<E> x = first; x != null; x = x.next)
s.writeObject(x.item);
}

17.反序列化链表

#readObject(java.io.ObjectInputStream s) 方法,反序列化数组。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// LinkedList.java

@java.io.Serial
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
// 读取非静态属性、非 transient 属性
s.defaultReadObject();

// Read in size
// 读取 size
int size = s.readInt();

// Read in all elements in the proper order.
// 顺序遍历,逐个反序列化
for (int i = 0; i < size; i++)
linkLast((E)s.readObject()); // 添加到链表尾部
}

18.克隆

#clone() 方法,克隆 LinkedList 对象。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// LinkedList.java

public Object clone() {
// 调用父类,进行克隆
LinkedList<E> clone = superClone();

// Put clone into "virgin" state
// 重置 clone 为初始化状态
clone.first = clone.last = null;
clone.size = 0;
clone.modCount = 0;

// Initialize clone with our elements
// 遍历遍历,逐个添加到 clone 中
for (Node<E> x = first; x != null; x = x.next)
clone.add(x.item);

return clone;
}
  • 注意,firstlast 等都是重新初始化进来,不与原 LinkedList 共享。

19.创建子数组

#subList(int fromIndex, int toIndex) 方法,创建 ArrayList 的子数组。代码如下:

1
2
3
4
5
6
7
8
9
// AbstractList.java

public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size());
// 根据判断 RandomAccess 接口,判断是否支持随机访问
return (this instanceof RandomAccess ?
new RandomAccessSubList<>(this, fromIndex, toIndex) :
new SubList<>(this, fromIndex, toIndex));
}
  • 该方法,是通过父类 AbstractList 来实现的。
  • 根据判断 RandomAccess 接口,判断是否支持随机访问,从而创建 RandomAccessSubList 或 SubList 对象

20.创建 Iterator 迭代器

#iterator() 方法,创建迭代器。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// AbstractSequentialList.java
public Iterator<E> iterator() {
return listIterator();
}

// AbstractList.java
public ListIterator<E> listIterator() {
return listIterator(0);
}

// AbstractSequentialList.java
public abstract ListIterator<E> listIterator(int index);

  • 该方法,是通过父类 AbstractSequentialList 来实现的。
  • 整个调用过程是,iterator() => listIterator() => listIterator(int index) 的顺序,就是我们在代码里贴进去的顺序。

21.创建 ListIterator 迭代器

#listIterator(int index) 方法,创建 ListIterator 迭代器。代码如下:

1
2
3
4
5
6
// LinkedList.java

public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}

创建ListItr迭代器

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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// LinkedList.java

private class ListItr implements ListIterator<E> {

/**
* 最后返回的节点
*/
private Node<E> lastReturned;
/**
* 下一个节点
*/
private Node<E> next;
/**
* 下一个访问元素的位置,从下标 0 开始。
*
* 主要用于 {@link #nextIndex()} 中,判断是否遍历结束
*/
private int nextIndex;
/**
* 创建迭代器时,数组修改次数。
*
* 在迭代过程中,如果数组发生了变化,会抛出 ConcurrentModificationException 异常。
*/
private int expectedModCount = modCount;

ListItr(int index) {
// assert isPositionIndex(index);
// 获得下一个节点
next = (index == size) ? null : node(index);
// 下一个节点的位置
nextIndex = index;
}

public boolean hasNext() {
return nextIndex < size;
}

public E next() {
// 校验是否数组发生了变化
checkForComodification();
// 如果已经遍历到结尾,抛出 NoSuchElementException 异常
if (!hasNext())
throw new NoSuchElementException();

// lastReturned 指向,记录最后访问节点
lastReturned = next;
// next 指向,下一个节点
next = next.next;
// 下一个节点的位置 + 1
nextIndex++;
// 返回 lastReturned
return lastReturned.item;
}

public boolean hasPrevious() {
return nextIndex > 0;
}

public E previous() {
// 校验是否数组发生了变化
checkForComodification();
// 如果已经遍历到结尾,抛出 NoSuchElementException 异常
if (!hasPrevious())
throw new NoSuchElementException();

// 修改 lastReturned 和 next 的指向。此时,lastReturned 和 next 是相等的。
lastReturned = next = (next == null) ? last : next.prev;
// 下一个节点的位置 - 1
nextIndex--;
// 返回 lastReturned
return lastReturned.item;
}

public int nextIndex() {
return nextIndex;
}

public int previousIndex() {
return nextIndex - 1;
}

public void remove() {
// 校验是否数组发生了变化
checkForComodification();
// 如果 lastReturned 为空,抛出 IllegalStateException 异常,因为无法移除了。
if (lastReturned == null)
throw new IllegalStateException();

// 获得 lastReturned 的下一个
Node<E> lastNext = lastReturned.next;
// 移除 lastReturned 节点
unlink(lastReturned);
// 此处,会分成两种情况
if (next == lastReturned) // 说明发生过调用 `#previous()` 方法的情况,next 指向下一个节点,而 nextIndex 是无需更改的
next = lastNext;
else
nextIndex--; // nextIndex 减一。

// 设置 lastReturned 为空
lastReturned = null;
// 增加数组修改次数
expectedModCount++;
}

public void set(E e) {
// 如果 lastReturned 为空,抛出 IllegalStateException 异常,因为无法修改了。
if (lastReturned == null)
throw new IllegalStateException();
// 校验是否数组发生了变化
checkForComodification();
// 修改 lastReturned 的 item 为 e
lastReturned.item = e;
}

public void add(E e) {
// 校验是否数组发生了变化
checkForComodification();
// 设置 lastReturned 为空
lastReturned = null;
// 此处,会分成两种情况
if (next == null) // 如果 next 已经遍历到尾,则 e 作为新的尾节点,进行插入。算是性能优化
linkLast(e);
else // 插入到 next 的前面
linkBefore(e, next);
// nextIndex 加一。
nextIndex++;
// 增加数组修改次数
expectedModCount++;
}

public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
// 遍历剩余链表
while (modCount == expectedModCount && nextIndex < size) {
// 执行 action 逻辑
action.accept(next.item);
// lastReturned 指向 next
lastReturned = next;
// next 指向下一个节点
next = next.next;
// nextIndex 加一。
nextIndex++;
}
// 校验是否数组发生了变化
checkForComodification();
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

}

小结

  • LinkedList 基于节点实现的双向链表的 List ,每个节点都指向前一个和后一个节点从而形成链表。

  • LinkedList 提供队列、双端队列、栈的功能。

    因为 first 节点,所以提供了队列的功能的实现的功能。
    因为 last 节点,所以提供了栈的功能的实现的功能。
    因为同时具有 first + last 节点,所以提供了双端队列的功能。

  • LinkedList 随机访问平均时间复杂度是 O(n) ,查找指定元素的平均时间复杂度是 O(n) 。

  • LinkedList 移除指定位置的元素的最好时间复杂度是 O(1) ,最坏时间复杂度是 O(n) ,平均时间复杂度是 O(n) 。

    最好时间复杂度发生在头部、或尾部移除的情况。

  • LinkedList 移除指定位置的元素的最好时间复杂度是 O(1) ,最坏时间复杂度是 O(n) ,平均时间复杂度是 O(n) 。

    最好时间复杂度发生在头部移除的情况。

  • LinkedList 添加元素的最好时间复杂度是 O(1) ,最坏时间复杂度是 O(n) ,平均时间复杂度是 O(n) 。

    最好时间复杂度发生在头部、或尾部添加的情况。

因为 LinkedList 提供了多种添加、删除、查找的方法,会根据是否能够找到对应的元素进行操作,抛出 NoSuchElementException 异常。整理了一个表格,避免错误使用。

返回结果 抛出异常
添加 #add(…)#offset(...)
删除 #remove(int index)#remove(E e)#poll(E E) #remove()
查找 #get(int index)#peek() #poll()

这个表主要整理了 List 和 Queue 的操作,暂时没有整理 Deque 的操作。因为,Deque 相同前缀的方法,表现结果同 Queue 。

拓展,在 Redis List 的数据结构,实现方式是类似 Java LinkedList 的方式

哈希表 HashMap

1.简介

HashMap ,是一种散列表,用于存储 key-value 键值对的数据结构,一般翻译为“哈希表”,提供平均时间复杂度为 O(1) 的、基于 key 级别的 get/put 等操作。

在日常的业务开发中,HashMap 可以说是和 ArrayList 一样常用的集合类,特别是考虑到数据库的性能,又或者服务的拆分后,我们把关联数据的拼接,放到了内存中,这就需要使用到 HashMap 了。

2. 类图

HashMap 实现的接口、继承的抽象类,如下图所示:

image-20230403110233600

3. 属性

在初次看到 HashMap 时,惊奇于其 O(1) 的 get 操作的时间复杂度。当时在我们已知的数据结构中,只有基于下标访问数组时,才能提供 O(1) get 操作的时间复杂度。

实际上,HashMap 所提供的 O(1) 是平均时间复杂度,大多数情况下保证 O(1) 。其实极端情况下,有可能退化为 O(N) 的时间复杂度噢,这又是为什么呢?

基础实现

HashMap 其实是在数组的基础上实现的,一个“加强版”的数组。如下图所示:

image-20230403111515130

key如果是一个整数,可以放入指向数组中的指定下标,但是key并不是整数。所以,hash 就正式登场了,通过 hash(key) 的过程,可以将 key 成功的转成一个整数。但是,hash(key) 可能会超过数组的容量,所以需要 hash(key) % size 作为下标,放入数组的对应位置。至此,是不是已经可以通过 O(1) 的方式,快速的从 HashMap 中进行 get 读取操作了。

注意,一般每个数组的“位置”,比较专业的说法,叫做“槽位”(slot)或者“桶”。

保证唯一

其实上述还是不太对,原因有两点:

  • 1、hash(key) 计算出来的哈希值,并不能保证唯一;
  • 2、hash(key) % size 的操作后,即使不同的哈希值,也可能变成相同的结果。

这样,就导致我们常说的“哈希冲突”。那么怎么解决呢?方法有两种:

  • 开放寻址法
  • 链表法

    在 Java HashMap 中,采用了链表法。 Redis Hash 数据结构也是采用了链表法。通过将数组的每个元素对应一个链表,将相同的 hash(key) % size 放到对应下标的链表中即可,put / get操作需要做下是否等于指定 key 的判断。

解决极端情况

其实上述还是会出现O(N) 的时间复杂度的情况,如果我们放入的 N 个 key-value 键值对到 HashMap 的情况:

  • 1、每个 key 经过 hash(key) % size 对应唯一下标,则 get 时间复杂度是 O(1) 。
  • 2、k 个 key 经过 hash(key) % size 对应唯一下标,那么在 get 这 k 个 key 的时间复杂度是 O(k) 。
  • 3、在情况 2 的极端情况下,k 恰好等于 N ,那么是不是就出现我们在上面说的 O(N) 的时间复杂度的情况。

为了解决最差 O(N) 的时间复杂度的情况,可以将数组的每个元素对应成其它数据结构,例如说:1)==红黑树==;2)==跳表==。它们两者的时间复杂度是 O(logN),这样 O(N) 就可以缓解成 O(logN) 的时间复杂度。

  • 在 JDK7 的版本中,HashMap 采用“数组 + 链表”的形式实现。

  • 在 JDK8 开始的版本,HashMap 采用“数组 + 链表 + 红黑树”的形式实现,在空间和时间复杂度中做取舍。

    这一点和 Redis 是相似的,即使是一个数据结构,可能内部采用多种数据结构,混合实现,为了平衡空间和时间复杂度。毕竟,时间不是唯一的因素,我们还需要考虑内存的情况。

如此,HashMap 的整体结构如下图:

image-20230403112917007

扩容

我们是希望 HashMap 尽可能能够达到 O(1) 的时间复杂度,链表法只是我们解决哈希冲突的无奈之举。而在 O(1) 的时间复杂度,基本是“一个萝卜一个坑”,所以在 HashMap 的 key-value 键值对数量达到阀值后,就会进行扩容

那么阀值是什么,又是怎么计算呢?此时就引入负载因子的概念。我们假设 HashMap 的数组容量为 capacity ,key-value 键值对数量为 size ,负载因子为 loadFactor 。那么,当 capacity / size > loadFactor 时,也就是使用的数组大小到达 loadFactor 比例时,我们就需要进行扩容。如此,我们便可以尽量达到“一个萝卜一个坑”的目的,从而尽可能的 O(1) 的时间复杂度。

下面,我们来看看 HashMap 的属性。代码如下:

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
// HashMap.java

/* ---------------- Fields -------------- */

/**
* 底层存储的数组
*/
transient Node<K,V>[] table;

/**
* 调用 `#entrySet()` 方法后的缓存
*/
transient Set<Map.Entry<K,V>> entrySet;

/**
* key-value 的键值对数量
*/
transient int size;

/**
* HashMap 的修改次数
*/
transient int modCount;

/**
* 阀值,当 {@link #size} 超过 {@link #threshold} 时,会进行扩容
*/
int threshold;

/**
* 扩容因子
*/
final float loadFactor;

table Node 数组。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// HashMap.java#Node.java

static class Node<K,V> implements Map.Entry<K,V> {

/**
* 哈希值
*/
final int hash;
/**
* KEY 键
*/
final K key;
/**
* VALUE 值
*/
V value;
/**
* 下一个节点
*/
Node<K,V> next;

// ... 省略实现方法

}
  • 实现了 Map.Entry 接口,该接口定义在 java.util.Map 接口中。
  • hash + key + value 属性,定义了 Node 节点的 3 个重要属性。
  • next 属性,指向下一个节点。通过它可以实现 table 数组的每一个位置可以形成链表。

Node 子类如下图:

image-20230403113838620

  • TreeNode ,定义在 HashMap 中,红黑树节点。通过它可以实现 table 数组的每一个位置可以形成红黑树。

4. 构造方法


本站由 @ToBe 使用[hexo]的Stellar 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本"" 访问 次 | 👀本站总访问 次 | 🥷本站总访客