深入Java集合框架:数组与List互转底层原理剖析(JDK 8)
前言
这是最近学习Java集合框架时做的一篇笔记,主要记录了数组与
ArrayList相互转换的底层原理和一些个人理解。内容比较基础,算是对自己学习过程的一个梳理,如果有不准确的地方欢迎指正
一、数组转List
1. 使用Arrays.asList()方法
public static void testArray2List() {
String[] strs = {"aaa", "bbb", "ccc"};
List<String> list = Arrays.asList(strs);
for (String s : list) {
System.out.println(s);
}
}
源码剖析:
Arrays.asList()方法的源码实现如下:
// Arrays 类中的方法
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);// 注意:此 ArrayList 是 Arrays 的内部类
}
这里返回的ArrayList并不是我们常用的java.util.ArrayList,而是Arrays类内部的一个静态内部类:
// Arrays 的内部类(不是 java.util.ArrayList!)
private static class ArrayList<E> extends AbstractList<E> {
private final E[] a; // 直接持有传入数组的引用
ArrayList(E[] array) {
a = array; // 没有拷贝,直接存引用
}
@Override
public E get(int index) {
return a[index]; // 直接读原数组
}
@Override
public E set(int index, E element) {
E oldValue = a[index];
a[index] = element; // 直接改原数组
return oldValue;
}
// 没有重写 add() / remove()
}
关键点:
-
这个内部类
ArrayList直接持有了原始数组的引用(通过构造函数传入) -
它没有实现
add()、remove()等方法(会抛出UnsupportedOperationException) -
对List的修改会直接影响原始数组
2. 转换后修改元素的影响
修改List元素:
List<String> list = Arrays.asList(strs);
list.set(0, "ddd"); // 合法操作
System.out.println(strs[0]); // 输出"ddd",原始数组也被修改
修改数组元素:
strs[0] = "eee";
System.out.println(list.get(0)); // 输出"eee",List也被修改
尝试添加/删除元素:
list.add("fff"); // 抛出 UnsupportedOperationException
list.remove(0); // 抛出 UnsupportedOperationException
我的理解
从源码可以看出几点:
-
它返回的根本不是
java.util.ArrayList,而是Arrays自己的一个内部类,这个内部类直接继承自AbstractList。 -
内部没有拷贝任何数据,只是把原数组的引用存到了字段
a里。 -
get()和set()操作的都是这个字段a,也就是直接操作原数组。
所以本质上,Arrays.asList() 做的事情是:给原数组套了一层 List 的壳,让你可以以 List 的方式去读写原数组。
这也解释了两个常见现象:
-
为什么不能
add/remove? —— 内部类没有重写这两个方法,调用父类AbstractList的默认实现会直接抛UnsupportedOperationException;而且数组长度本身就是固定的,也没法真正"加"。 -
为什么修改 List 元素会同步影响原数组? —— 因为本来就是同一块内存,List 内部字段
a指向的就是你传进来的那个数组对象。
如果用一句话来概括:
Arrays.asList()返回的是一个零拷贝的数组视图,不是一个真正的动态数组。
如果你需要一个独立的、可修改的 ArrayList,标准写法是:
List<String> list = new ArrayList<>(Arrays.asList(array));
这样会通过 ArrayList 的构造函数把数据拷贝一份,得到一个真正属于自己的容器。
二、List转数组
1. 使用toArray()方法
public static void testList2Array() {
List<String> list = new ArrayList<String>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
String[] array = list.toArray(new String[list.size()]);
for (String s : array) {
System.out.println(s);
}
}
源码剖析:
ArrayList.toArray()方法的源码实现:
public <T> T[] toArray(T[] a) {
if (a.length < size)
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
Arrays.copyOf() 和 System.arraycopy() 做的事情可以概括为:
-
创建一个新的数组对象(容器是新的)
-
把原 List 内部数组中的每个元素引用,逐个拷贝到新数组中(元素引用是旧的)
关键点:
-
toArray()方法会创建一个新的数组(即使传入的数组长度足够,也会复制元素) -
新数组与原List是完全独立的,互不影响
-
传入的数组参数主要用于指定返回数组的类型
2. 转换后修改元素的影响
修改List元素:
List<String> list = new ArrayList<>();
list.add("aaa");
String[] array = list.toArray(new String[0]);
list.set(0, "bbb");
System.out.println(array[0]); // 输出"aaa",数组不受影响
修改数组元素:
array[0] = "ccc";
System.out.println(list.get(0)); // 输出"aaa",List不受影响
我的理解
-
容器层面:新数组是独立
new出来的,所以往新数组里赋值一个新元素(array[0] = new String("X")),不会影响原 List。 -
元素层面:拷贝的只是引用地址,如果元素是可变对象,通过任意一端修改对象的属性,另一端也会看到变化。这就是浅拷贝的语义。
基本类型数组(
int[]、long[]等)因为直接拷贝值,不存在引用共享的问题,天然就是完全独立的。
三、完整示例与验证
import java.util.*;
public class ArrayToListConversion {
// 数组转List
public static void testArray2List() {
System.out.println("=== 数组转List ===");
String[] strs = {"aaa", "bbb", "ccc"};
List<String> list = Arrays.asList(strs);
// 打印原始数组
System.out.println("原始数组: " + Arrays.toString(strs));
// 修改List元素
list.set(0, "ddd");
System.out.println("修改List后数组: " + Arrays.toString(strs));
// 修改数组元素
strs[1] = "eee";
System.out.println("修改数组后List: " + list);
// 尝试添加元素
try {
list.add("fff");
} catch (UnsupportedOperationException e) {
System.out.println("添加元素失败: " + e.getMessage());
}
}
// List转数组
public static void testList2Array() {
System.out.println("\n=== List转数组 ===");
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
// 转换为数组
String[] array = list.toArray(new String[0]);
// 打印原始List
System.out.println("原始List: " + list);
// 修改List元素
list.set(0, "ddd");
System.out.println("修改List后数组: " + Arrays.toString(array));
// 修改数组元素
array[1] = "eee";
System.out.println("修改数组后List: " + list);
}
public static void main(String[] args) {
testArray2List();
testList2Array();
}
}
输出结果:
=== 数组转List ===
原始数组: [aaa, bbb, ccc]
修改List后数组: [ddd, bbb, ccc]
修改数组后List: [ddd, eee, ccc]
添加元素失败: Unsupported operation
=== List转数组 ===
原始List: [aaa, bbb, ccc]
修改List后数组: [aaa, bbb, ccc]
修改数组后List: [ddd, bbb, ccc]
四、关键结论
-
数组转List (
Arrays.asList()):-
返回的List是原始数组的"视图",共享同一内存空间
-
对List的修改会直接影响原始数组
-
对数组的修改也会影响List
-
不支持添加/删除操作(会抛出
UnsupportedOperationException)
-
-
List转数组 (
toArray()):-
返回一个全新的数组,与原始List完全独立
-
对List的修改不会影响数组
-
对数组的修改也不会影响List
-
推荐使用
list.toArray(new T[0])方式(Java 6+中性能更好)
-
-
使用建议:
-
如果需要一个可修改大小的List,应将
Arrays.asList()的结果包装进new ArrayList<>() -
如果需要完全独立的数组,应使用
toArray()方法 -
在处理基本类型数组时需特别注意(基本类型数组转List有特殊处理)
-
理解这些转换机制和相互影响关系,对于编写健壮的Java代码非常重要,特别是在处理集合和数组混合使用的场景时。
写在最后
以上就是这次关于数组与
ArrayList转换的学习记录。后续我还会继续整理深浅拷贝、基本类型数组转换等其他相关话题,慢慢补全这个系列。如果文中有理解偏差的地方,欢迎大家在评论区交流指正~
浙公网安备 33010602011771号