new ArrayList<>() 与 new ArrayList<>(0)有什么区别。

无用知识点系列一:new ArrayList<>() 与 new ArrayList<>(0)有什么区别。

ArrayList中的两个空数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static final Object[] EMPTY_ELEMENTDATA = {};

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}

public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

在ArrayList的源码中,分别有两个空数组对象 EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,他们的区别是在于有参构造使用 EMPTY_ELEMENTDATA 而无参构造则使用 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。在现在,你可能会觉得这玩的什么蛇皮操作,为什么不统一改成 EMPTY_ELEMENTDATA 或者 DEFAULTCAPACITY_EMPTY_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
transient Object[] elementData; // list内部存储的数据

private static final int DEFAULT_CAPACITY = 10; // 默认扩容容量

public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
// 这个是用于 fast-fail(快速失败),非 Iterator 迭代若是使用了增删改操作
// modCount 会递增导致和迭代时保存的 expectedModCount 不一致而抛出异常。
modCount++;

// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}


private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

首先我们看看扩容的逻辑: ==oldCapacity + (oldCapacity >> 1)== 。说明容器扩容并非 1.5 倍,因为没有小数位,所以当尾数不为 0 的话,就相当于舍去了 0.5,所以扩容为 1.5 倍舍去余数。至于为什么我感觉大家二进制都会,就不多叭叭了。

接下来是 add 方法,add 的时候,elementData 等于 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的时候,是会直接给数组扩容成 10 的大小,如果是 EMPTY_ELEMENTDATA 则不做处理,直接扩容成了 1。

由此可得:

  1. 如果是无参构造的话,容器的容量为:0、10、15、22、33 …
  2. 如果是有参构造且参数为 0,容器的容量为:0、1、2、3、4、6、9、13 …

我们会发现无参构造容器的大小只需要一次扩容就能到 10,而有参构造且参数为 0 的时候,需要七次才能突破 10 的大关。为什么作者这么设计呢?我觉得可能是作者考虑到了有这么一种场景:集合包含的元素不确定,但基本数量都会小于 10,这时使用有参构造且参数为 0 则可以大量的节省内存。

假设我们有个百万级别并发访问的api,返回的数组都是两三个,这时候用有参为 0 的构造方法,则可以节省 (10 - 2) * 10 000 个数组空间,四舍五入相当于一个亿。

好吧,我真不知道这个设计有啥用,我猜想也没有几个人会用这种有参构造方法吧。

作者的话

今天的每天一个没用知识点结束,是不是学完感觉到浪费了两分钟时间,每天一个知识点,一年就是 365 * 2 / 60 = 12 个小时,坚持一年就浪费了半天时间,离废物又能更近一步了。