本文目的是从源码层面分清楚 List 中这两个函数的区别:

  • Object[] toArray()
  • <T> T[] toArray(T[] a)。参数 a 由用户传递,用于指定新数组生成的位置。

快速感知

两个函数的作用都是将 ArrayList 对象转换为数组,都返回新数组,多次运行 toArray 方法会获得不同的数组对象,但是这些数组对象中内容一样的。

区别在于是否携带参数。具体区别:

  • Object[] toArray()
    • 如果 ArrayList 中的内容是基本类型,你可以简单的认为新数组之间没有任何关联。
    • 如果 ArrayList 中的内容是自定义类型,那么即使 toArray 返回不同的新数组,但是在不同的新数组中,对应下标元素均引用同一个对象。
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
// 自定义类型
private static class MyPair{
int val;
public MyPair(int val){this.val = val;}
@Override
public String toString() {
return "MyPair{ val = "+ this.val + '}';
}
}
public static void main(String[] args) {
List<MyPair> ls =new ArrayList<>();
ls.add(new MyPair(0));
ls.add(new MyPair(1));
ls.add(new MyPair(2));

Object[] newArr1 = ls.toArray();
Object[] newArr2 = ls.toArray();
System.out.println(newArr1==newArr2); // false
System.out.println(newArr1[0]==newArr2[0]); // true

newArr1[0] = new MyPair(99); // 对第一个数组内容进行修改 (new 新对象)
System.out.println(Arrays.toString(newArr1));
System.out.println(Arrays.toString(newArr2));
// 输出:
// [MyPair{ val = 99}, MyPair{ val = 1}, MyPair{ val = 2}]
// [MyPair{ val = 0}, MyPair{ val = 1}, MyPair{ val = 2}]

((MyPair)newArr2[1]).val = 66; // 对第二个数字内容指向的对象进行修改
System.out.println(Arrays.toString(newArr1));
System.out.println(Arrays.toString(newArr2));
// 输出:
// [MyPair{ val = 99}, MyPair{ val = 66}, MyPair{ val = 2}]
// [MyPair{ val = 0}, MyPair{ val = 66}, MyPair{ val = 2}]

}
  • <T> T[] toArray(T[] a)a 指的是用户可以指定的新数组的存储位置,返回类型为 T[]。如果用户给的 a 足够大,代码就会把元素复制到 a 中,如果还有富余,则结尾设置 null;如果不够大,则代码自己生成一个大小刚好符合要求的新的数组,再复制返回。
    • ls.toArray(new Object[0]) 等同于 ls.toArray()
阿里巴巴 Java 开发手册对集合使用的相关要求

一、(六)9.【强制】 使用集合转数组的方法, 必须使用集合的 toArray(T[] array), 传入的是类型完全一致、 长度为 0 的空数组。

原因:

  1. 直接使用 toArray 无参方法存在问题, 此方法返回值只能是 Object[] 类, 若强转其它类型数组将出现 ClassCastException 错误。
  2. 使用 toArray 带参方法, 数组空间大小的 length 的选择:
    • 等于 0, 动态创建与 size 相同的数组, 性能最好。
    • 大于 0 但小于 size, 重新创建大小等于 size 的数组, 增加 GC 负担。也就是说,作为参数传入的那个数组没用上。
    • 等于 size, 在高并发情况下, 数组创建完成之后, size 正在变大的情况下, 负面影响与第 2 种情况相同。但实际上,并发场景本就不建议使用 ArrayList
    • 大于 size, 空间浪费, 且在 size 处插入 null 值, 存在 NPE 隐患。

源码阅读

此部分需读者耐心阅读。

System.arraycopy()Arrays.copyOf() 方法

为了便于后面内容的理解,这里先介绍 System.arraycopy()Arrays.copyOf() 方法。

1
2
3
4
5
6
7
8
9
10
11
/** System
* 复制数组
* @param src 源数组
* @param srcPos 源数组中的起始位置
* @param dest 目标数组
* @param destPos 目标数组中的起始位置
* @param length 要复制的数组元素的数量
*/
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] a = new int[]{0,1,2,3,0,0,0,0,0,0,0};
System.out.println("执行前:");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
System.arraycopy(a, 2, a, 7, 3);
System.out.println("\n执行后:");
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
// 执行前:
// 0 1 2 3 0 0 0 0 0 0 0
// 执行后:
// 0 1 2 3 0 0 0 2 3 0 0

Arrays.copyOf() 返回一个全新数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Arrays.copyOf() 的其中两个重写
// 有返回值为 byte[] short[] 等类型的重写
public static int[] copyOf(int[] original, int newLength) {
int[] copy = new int[newLength]; // 新数组可用于扩容
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}

// Arrays 类
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

ArrayList 类中的 toArray

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ArrayList 类
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}

public <T> T[] toArray(T[] a) {
// 如果用户传入的 a 的大小过小,代码自动为你创建一个合适的新数组,并执行复制
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
// 如果 a 的大小富余,就直接用用户的。
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null; // 最后置 null 代表结束
return a;
}

【拓展】使用原有 Collection 类构造新的 ArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static final Object[] EMPTY_ELEMENTDATA = {};
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray(); // 不是所有集合的 toArray() 方法都遵循同样的实现细节
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
// 防御性编程。如果 c 的类型不是 ArrayList,c.toArray() 返回的数组可能并不是完全适配于 ArrayList 的内部需求(如具体的实现或内存管理)。因此,通过 Arrays.copyOf 创建一个新的数组,确保 ArrayList 自己拥有一个与其内部规范一致的新数组。
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}

小结:

  • 如果传入的集合类 c 大小为空,则元素列表 elementData 设为空集合。
  • 如果传入的集合类 c 大小不为空,则将 c 中的数组处理好。

本文参考

推荐阅读:一个 Bug JDK 居然改了十年?