数组复制是编程开发中非常常见的一类操作,尤其是在数据处理、集合运算、算法实现以及业务数据隔离场景中。不同编程语言对数组的定义、内存模型和复制机制存在明显差异,因此“复制一个数组”并不只是简单地把数据赋值给另一个变量。
实际开发中需要重点区分浅复制、深复制、引用复制三种情况。对于基本类型数组,直接复制通常不会产生复杂问题;但当数组包含对象、集合或其他引用类型时,如果复制方式选择不当,就可能出现修改一个数组导致另一个数组数据同步变化的问题。
下面结合 Java、JavaScript、Python、C++、C#、Go、PHP 等常见编程语言,介绍数组复制的常用方法及其适用场景。
一、Java实现数组复制
Java 中数组属于对象类型,数组变量保存的是数组对象的引用。因此:
Javaint[] a = {1, 2, 3, 4}; int[] b = a;
这种写法并没有真正复制数组,而是让 a 和 b 指向同一个数组。
修改 b:
Javab[0] = 100; System.out.println(a[0]);
输出:
100
如果需要创建一个独立数组,可以使用以下几种方式。
1. 使用Arrays.copyOf()
这是 Java 中比较常用、可读性较好的数组复制方式:
Javaimport java.util.Arrays; int[] source = {1, 2, 3, 4, 5}; int[] target = Arrays.copyOf(source, source.length);
复制后的 target 是新的数组对象。
还可以指定复制长度:
Javaint[] target = Arrays.copyOf(source, 3);
结果为:
[1, 2, 3]
如果指定的长度大于原数组长度,多出来的位置会使用默认值填充。
2. 使用System.arraycopy()
对于需要复制部分数组或者对性能要求较高的场景,可以使用:
Javaint[] source = {1, 2, 3, 4, 5}; int[] target = new int[5]; System.arraycopy(source, 0, target, 0, source.length);
参数分别表示源数组、源数组起始位置、目标数组、目标数组起始位置以及复制长度。
例如:
JavaSystem.arraycopy(source, 1, target, 0, 3);
表示从 source 的下标 1 开始复制三个元素。
3. 使用clone()
数组还可以通过 clone() 完成复制:
Javaint[] source = {1, 2, 3}; int[] target = source.clone();
这种方式简单直接,适用于复制整个数组。
需要注意的是,Java 数组的 clone() 属于浅复制。如果数组元素本身是对象,那么对象引用仍然指向原来的对象。
二、JavaScript实现数组复制
JavaScript 中数组属于对象,直接赋值同样属于引用复制:
JavaScriptconst source = [1, 2, 3]; const target = source; target[0] = 100; console.log(source);
结果:
[100, 2, 3]
因此需要根据数据结构选择正确的复制方法。
1. 使用扩展运算符
最常见的方式是:
JavaScriptconst source = [1, 2, 3, 4]; const target = [...source];
也可以使用:
JavaScriptconst target = [...source];
对于只包含基本类型元素的一维数组,这种方式非常方便。
2. 使用slice()
JavaScriptconst source = [1, 2, 3, 4]; const target = source.slice();
slice() 不传参数时,会返回一个新的数组。
如果只需要复制部分内容:
JavaScriptconst target = source.slice(1, 3);
得到:
JavaScript[2, 3]
3. 使用Array.from()
JavaScriptconst source = [1, 2, 3]; const target = Array.from(source);
这种写法同样可以创建新的数组。
4. 对象数组需要注意浅复制
例如:
JavaScriptconst source = [ { name: "Tom" }, { name: "Jerry" } ]; const target = [...source]; target[0].name = "Jack"; console.log(source[0].name);
结果仍然是:
Jack
原因在于扩展运算符只复制数组结构,并没有复制数组中的对象。
如果数据结构比较简单,可以使用:
JavaScriptconst target = structuredClone(source);
structuredClone() 可以对支持结构化克隆的数据进行深层复制,比简单的 JSON 序列化方式更加可靠。
三、Python实现列表复制
Python 中通常使用 list 表示数组类型的数据结构。
直接赋值:
Python运行source = [1, 2, 3] target = source
此时两个变量引用的是同一个列表。
1. 使用copy()
Python运行source = [1, 2, 3] target = source.copy()
也可以使用:
Python运行target = list(source)
或者:
Python运行target = source[:]
这几种方式都可以实现一层复制。
2. 使用copy模块进行深复制
当列表内部包含嵌套对象时,可以使用:
Python运行import copy source = [[1, 2], [3, 4]] target = copy.deepcopy(source)
修改:
Python运行target[0][0] = 100
不会影响原来的 source。
需要注意:
Python运行target = source.copy()
只能复制外层列表:
Python运行source = [[1, 2], [3, 4]] target = source.copy() target[0][0] = 100
此时 source[0][0] 也会变成 100。
四、C++实现数组复制
C++ 中既有传统数组,也有 std::array、std::vector 等容器,因此复制方式有所不同。
1. 使用std::copy复制普通数组
C++#includeint source[] = {1, 2, 3, 4, 5}; int target[5]; std::copy(source, source + 5, target);
复制完成后,target 拥有独立的数据。
2. 使用std::array
如果使用 std::array:
C++#includestd::array<int, 3> source = {1, 2, 3}; std::array<int, 3> target = source;
这种方式非常简洁。
3. 使用std::vector
动态数组通常推荐使用 std::vector:
C++#includestd::vector<int> source = {1, 2, 3}; std::vector<int> target = source;
vector 的拷贝构造会创建独立的数据存储空间,因此修改 target 不会影响 source。
例如:
C++target[0] = 100;
不会修改原来的 source[0]。
五、C#实现数组复制
C# 中数组可以通过 Clone()、Array.Copy() 等方式复制。
1. 使用Clone()
C#int[] source = { 1, 2, 3, 4 }; int[] target = (int[])source.Clone();
这种方式适合完整复制数组。
2. 使用Array.Copy()
C#int[] source = { 1, 2, 3, 4 }; int[] target = new int[source.Length]; Array.Copy(source, target, source.Length);
如果需要复制部分内容:
C#Array.Copy(source, 1, target, 0, 2);
表示从源数组下标 1 开始复制两个元素。
3. 使用LINQ
也可以使用:
C#using System.Linq; int[] target = source.ToArray();
这种写法代码比较直观,但单纯进行数组复制时,通常没有必要为了复制数组而额外引入 LINQ。
六、Go实现数组复制
Go 对数组的赋值具有值语义:
Gosource := [3]int{1, 2, 3} target := source
此时 target 是 source 的副本:
Gotarget[0] = 100
不会影响 source。
Go 中数组复制非常直接。
不过需要注意数组和切片的区别。
Gosource := []int{1, 2, 3} target := source
这里的 source 和 target 都是切片,它们可能共享底层数组。
如果希望复制一个独立切片,可以使用:
Gotarget := make([]int, len(source)) copy(target, source)
或者:
Gotarget := append([]int(nil), source...)
对于大型数据结构,正确区分数组和切片尤其重要。
七、PHP实现数组复制
PHP 中数组赋值具有比较特殊的语义:
PHP$source = [1, 2, 3]; $target = $source;
修改:
PHP$target[0] = 100;
通常不会影响 $source。
PHP 使用写时复制机制,在实际修改数据时才会进行必要的数据分离。
如果数组内部包含对象,则需要特别注意:
PHP$source = [ new User() ]; $target = $source;
此时数组虽然进行了复制,但其中的对象仍然是对象引用。修改对象属性可能影响原数组中的对象。
如果需要复制对象本身,需要结合对象的 clone 机制进行处理。
八、Kotlin实现数组复制
Kotlin 中可以使用 copyOf():
Kotlinval source = arrayOf(1, 2, 3, 4) val target = source.copyOf()
如果只复制部分元素:
Kotlinval target = source.copyOf(2)
得到前两个元素。
对于基本类型数组,也可以使用对应的方法:
Kotlinval source = intArrayOf(1, 2, 3) val target = source.copyOf()
如果数组中包含对象,copyOf() 同样主要是复制数组容器,而不是递归复制其中的对象。
九、不同编程语言数组复制方法对比
常见语言的数组复制方式可以概括为:
| 编程语言 | 常用复制方式 | 默认特性 |
|---|---|---|
| Java | Arrays.copyOf() | 浅复制 |
| Java | System.arraycopy() | 浅复制 |
| JavaScript | [...arr] | 浅复制 |
| JavaScript | slice() | 浅复制 |
| Python | copy() | 浅复制 |
| Python | copy.deepcopy() | 深复制 |
| C++ | std::copy() | 元素复制 |
| C++ | vector赋值 | 独立容器复制 |
| C# | Clone() | 浅复制 |
| C# | Array.Copy() | 元素复制 |
| Go | 数组直接赋值 | 值复制 |
| Go | copy() | 切片元素复制 |
| PHP | 直接赋值 | 写时复制机制 |
| Kotlin | copyOf() | 浅复制 |
从实际开发角度来看,不能仅仅记住某个函数名称,更重要的是理解它究竟复制了什么。
十、浅复制与深复制的区别
数组复制中最容易出现的问题,就是没有区分浅复制和深复制。
假设数组为:
[ {name: "Tom"}, {name: "Jerry"} ]
浅复制只创建一个新的数组容器:
新数组 → 对象A → 对象B 原数组 → 对象A → 对象B
两个数组虽然不同,但内部对象仍然是同一批对象。
深复制则会进一步创建新的对象:
新数组 → 对象A' → 对象B' 原数组 → 对象A → 对象B
因此,修改对象内部属性时,两个数组不会互相影响。
需要注意,深复制并不是简单地把“复制层数”增加,而是需要根据数据类型和语言提供的复制机制来决定具体实现方式。
十一、数组复制中的常见错误
1. 直接赋值误认为是复制
这是最常见的问题:
JavaScriptconst b = a;
或者:
Javaint[] b = a;
如果语言采用引用语义,这只是复制了引用,并没有创建新的数组。
2. 复制嵌套数组后直接修改内部元素
例如 JavaScript:
JavaScriptconst a = [[1, 2], [3, 4]]; const b = [...a]; b[0][0] = 100;
此时 a[0][0] 也会发生变化。
因此,对于二维数组或包含对象的数据结构,需要根据业务需求决定是否进行深复制。
3. 为了深复制盲目使用JSON
JavaScript 中经常看到:
JavaScriptconst b = JSON.parse(JSON.stringify(a));
对于简单数据确实可以达到深复制效果,但它存在明显限制。例如 Date、Map、Set、undefined、函数以及循环引用等情况都可能导致数据丢失或报错。
现代 JavaScript 环境中,如果数据符合结构化克隆规范,可以优先考虑:
JavaScriptconst b = structuredClone(a);
4. 忽略性能问题
复制数组意味着需要读取并写入元素。对于包含数百万甚至更多元素的大型数组,频繁复制可能带来明显的内存和性能开销。
如果数据并不需要独立修改,可以考虑使用只读结构、共享数据或其他更合适的数据结构,避免不必要的复制。
十二、如何选择数组复制方式
简单的一维基本类型数组通常直接使用语言提供的标准复制方法即可。
Java 可以优先考虑:
JavaArrays.copyOf(source, source.length);
JavaScript 可以使用:
JavaScriptconst target = [...source];
Python 可以使用:
Python运行target = source.copy()
C++ 可以直接进行容器复制:
C++std::vector<int> target = source;
C# 可以使用:
C#int[] target = (int[])source.Clone();
Go 的切片则推荐:
Gotarget := make([]int, len(source)) copy(target, source)
真正涉及对象、嵌套数组、集合等复杂结构时,则需要进一步确认是需要浅复制还是深复制。
数组复制看似简单,但其核心并不只是“如何写一行代码”,而是要明确数据是否需要独立、元素是否属于引用类型、是否存在嵌套结构以及复制操作是否会带来额外性能成本。掌握这些底层规则后,无论使用哪一种编程语言,都能更加准确地选择适合当前场景的数组复制方案。