1. 项目概述从“合并”到“高效合并”的思维跃迁“把两个数组合并成一个”这大概是每个Java初学者在接触数组后遇到的第一个像模像样的练习题。乍一看这问题简单得甚至有些无聊——不就是把两个东西拼在一起吗但如果你在面试中被问到或者在实际项目中需要处理大规模数据合并时还停留在for循环遍历复制的层面那可能就有点危险了。我见过不少简历上写着“精通Java”的候选人在这个基础问题上栽了跟头不是性能考虑不周就是边界情况处理不当。今天我们不只聊“如何合并”更要深挖“如何根据场景选择最优方式合并”。这背后涉及内存管理、算法效率、API特性乃至代码的可维护性。无论是应对java面试题中关于数组和对象数组去重的连环问还是解决实际开发中填充数据合并单元格、处理memoria合并video.m4s和audio.m4s这类数据拼接需求其核心逻辑都是相通的。掌握数组合并的“道”与“术”是理解Java集合框架、流式API乃至更高阶并发编程的一块重要基石。2. 核心思路拆解不止一种拼法当我们谈论合并两个数组时本质上是在创建一个新的、容量足够大的容器然后按顺序将两个源数组的所有元素“搬运”到这个新容器中。这个朴素的描述背后隐藏着几个必须明确的核心决策点这些点直接决定了后续实现方案的优劣。2.1 明确合并的“语义”首先我们必须定义清楚“合并”的具体含义这直接关系到后续的算法和结果简单拼接这是最常见的要求。将数组B的所有元素按顺序接在数组A的所有元素之后。例如A [1, 2, 3],B [4, 5]合并结果为[1, 2, 3, 4, 5]。这也是本文主要讨论的场景。有序合并前提是两个数组各自已经按某种规则如升序排好序合并后的新数组仍需保持有序。这通常需要使用双指针算法是归并排序的核心步骤复杂度可以做到O(n)。去重合并合并后新数组中不能有重复元素。这需要在搬运过程中加入查重逻辑可能涉及HashSet等数据结构或者先合并再去重。交错合并像洗牌一样从A和B中轮流取一个元素放入新数组直到某个数组耗尽再将剩余元素全部追加。我们的标题“两个数组合并为一个数组”在没有特殊说明的情况下通常指第一种简单拼接。2.2 技术选型的核心考量因素选择哪种实现方式绝不是随机的。你需要像一个架构师一样思考权衡以下几点性能数据量有多大是int这样的基本类型数组还是复杂的对象数组对于海量数据可能引发java: outofmemoryerror: insufficient memory内存复制速度至关重要。简洁性与可读性代码是写给人看的。在性能可接受的前提下优先选择意图清晰、易于维护的写法。System.arraycopy和Arrays.copyOf在标准库中经过极致优化通常比自己写的for循环更快、更可靠。泛型支持你处理的是int[]还是String[]或是自定义的User[]对于对象数组方法需要具备泛型能力。Arrays.copyOf因其泛型设计在处理对象数组时更加优雅安全。不可变性合并操作是否应该改变原始数组绝大多数情况下答案是否定的。我们应该创建并返回一个新数组保证源数据不被意外修改这符合函数式编程的“无副作用”思想能减少Bug。基于这些考量我们可以把常见的实现方案分为几个梯队最基础的循环手动复制、面向系统底层的批量拷贝、以及现代Java中更声明式的流式处理。3. 方案实现与深度剖析下面我将逐一拆解四种最具代表性的实现方案并附上详细的代码、原理解释以及我踩过的坑。3.1 方案一手动循环复制最直观但最原始这是新手最容易想到的方法也是理解合并过程本质的起点。public class ManualLoopMerge { public static int[] mergeArrays(int[] array1, int[] array2) { // 1. 创建足够容纳两个数组的新数组 int[] mergedArray new int[array1.length array2.length]; // 2. 复制第一个数组 for (int i 0; i array1.length; i) { mergedArray[i] array1[i]; } // 3. 复制第二个数组注意起始位置是第一个数组的末尾 for (int i 0; i array2.length; i) { mergedArray[array1.length i] array2[i]; } return mergedArray; } public static void main(String[] args) { int[] a {1, 2, 3}; int[] b {4, 5, 6}; int[] result mergeArrays(a, b); System.out.println(Arrays.toString(result)); // 输出: [1, 2, 3, 4, 5, 6] } }原理解读与注意事项时间复杂度O(nm)其中n和m分别是两个数组的长度。我们需要遍历每个元素一次。空间复杂度O(nm)必须创建一个新的数组来存放所有元素。关键下标计算第二个循环中目标数组的下标是array1.length i。这是最容易出错的地方之一新手常写成i导致数据覆盖。适用类型这个方法直接但只适用于基本类型数组如int[],double[]或你知道如何复制的对象数组。对于对象数组它复制的是对象的引用而非对象本身浅拷贝。踩坑实录早期我曾用这种方法合并两个ListString转换来的数组当时没意识到数组长度可能为0。如果array1.length为0第二个循环的起始索引计算0 i虽然没错但第一个循环的遍历 (i 0) 不会执行这没问题。但若array1为null则会抛出NullPointerException。任何数组合并方法首要步骤都应该是进行空值检查。3.2 方案二使用System.arraycopy()高性能的标准答案这是Java标准库提供的原生数组复制方法由JVM本地实现执行效率极高是处理大规模数组合并的首选方案。import java.util.Arrays; public class SystemArrayCopyMerge { public static int[] mergeArrays(int[] array1, int[] array2) { // 健壮性检查 if (array1 null) return array2 null ? new int[0] : Arrays.copyOf(array2, array2.length); if (array2 null) return Arrays.copyOf(array1, array1.length); // 创建目标数组 int[] mergedArray new int[array1.length array2.length]; // 参数含义: (源数组, 源起始位置, 目标数组, 目标起始位置, 要复制的元素数量) System.arraycopy(array1, 0, mergedArray, 0, array1.length); System.arraycopy(array2, 0, mergedArray, array1.length, array2.length); return mergedArray; } // 泛型版本支持任意类型的数组 public static T T[] mergeArrays(T[] array1, T[] array2) { if (array1 null) return array2 null ? null : Arrays.copyOf(array2, array2.length); if (array2 null) return Arrays.copyOf(array1, array1.length); // 注意这里创建与array1类型相同的新数组 T[] mergedArray Arrays.copyOf(array1, array1.length array2.length); System.arraycopy(array2, 0, mergedArray, array1.length, array2.length); return mergedArray; } }原理解读与性能优势JVM intrinsicSystem.arraycopy()是一个内在函数intrinsicJVM会识别这个方法调用并可能用高度优化的汇编指令如x86平台的REP MOVS来实现内存块的批量移动这比Java层面的循环快得多。参数设计它的参数设计非常灵活不仅可以用于合并还可以用于数组内元素的移动、部分复制等。类型安全在泛型版本中我们利用Arrays.copyOf(T[] original, int newLength)来创建新数组。这个方法会保留原始数组的组件类型避免了不安全的强制类型转换。内存处理它直接操作内存地址避免了Java层面每次赋值可能带来的额外检查尽管JIT也会优化循环在处理二维数组或大对象数组时优势明显。实操心得System.arraycopy()是处理数组复制包括合并的“瑞士军刀”。在java面试中如果你能提到这个方法并解释其高性能原理绝对是加分项。记住它的参数顺序有个口诀“源、源起、目、目起、长度”。在合并场景下第二个调用的“目标起始位置”永远是第一个数组的长度这是关键。3.3 方案三使用Arrays.copyOf()与System.arraycopy()组合更优雅的泛型方案方案二的泛型版本已经用到了Arrays.copyOf()。我们可以将其视为一个更上层的工具它内部其实也调用了System.arraycopy()但提供了更简洁的API来“创建并复制”。import java.util.Arrays; public class ArraysCopyOfMerge { public static T T[] mergeArrays(T[] first, T[] second) { // 处理空数组边界情况 if (first null || first.length 0) { return second null ? null : Arrays.copyOf(second, second.length); } if (second null || second.length 0) { return Arrays.copyOf(first, first.length); } // 1. 以第一个数组为模板扩展其长度 T[] result Arrays.copyOf(first, first.length second.length); // 2. 将第二个数组复制到扩展后的空间 System.arraycopy(second, 0, result, first.length, second.length); return result; } public static void main(String[] args) { String[] arr1 {Hello, World}; String[] arr2 {From, Java}; String[] merged mergeArrays(arr1, arr2); System.out.println(Arrays.toString(merged)); // [Hello, World, From, Java] // 也适用于自定义对象数组 Person[] people1 {new Person(Alice), new Person(Bob)}; Person[] people2 {new Person(Charlie)}; Person[] allPeople mergeArrays(people1, people2); } static class Person { String name; Person(String name) { this.name name; } Override public String toString() { return name; } } }方案优势分析代码简洁Arrays.copyOf()一行代码完成了“创建新数组复制原内容”两个操作意图非常清晰。类型推断它自动推断并保持数组的元素类型无需像手动创建泛型数组那样进行繁琐且不安全的转换。安全便捷这是Java标准库推荐的方式尤其在编写通用工具方法时能有效避免“未经检查的转换”警告。3.4 方案四使用Java 8 Stream API声明式编程对于喜欢函数式风格、或者合并前后还需要进行映射、过滤、去重等操作的场景Stream API提供了一种非常优雅的解决方案。import java.util.Arrays; import java.util.stream.IntStream; import java.util.stream.Stream; public class StreamMerge { // 合并基本类型int数组 public static int[] mergeIntArrays(int[] array1, int[] array2) { return IntStream.concat( Arrays.stream(array1), Arrays.stream(array2) ) .toArray(); } // 合并对象数组泛型 public static T T[] mergeObjectArrays(T[] array1, T[] array2, IntFunctionT[] generator) { return Stream.concat( Arrays.stream(array1), Arrays.stream(array2) ) .toArray(generator); } // 一个更实用的例子合并并去重 public static Integer[] mergeAndDistinct(Integer[] array1, Integer[] array2) { return Stream.concat( Arrays.stream(array1), Arrays.stream(array2) ) .distinct() // 去重 .sorted() // 排序可选 .toArray(Integer[]::new); // 使用方法引用生成目标数组 } public static void main(String[] args) { int[] a {1, 2, 3}; int[] b {3, 4, 5}; int[] mergedInts mergeIntArrays(a, b); System.out.println(Arrays.toString(mergedInts)); // [1, 2, 3, 3, 4, 5] Integer[] distinctResult mergeAndDistinct( new Integer[]{1, 2, 3}, new Integer[]{3, 4, 5} ); System.out.println(Arrays.toString(distinctResult)); // [1, 2, 3, 4, 5] } }适用场景与性能考量优势代码表达力强链式调用清晰描述了“合并-去重-排序”的整个流水线。非常适合与集合类List,Set转换配合使用或者在数据预处理阶段进行复杂操作。劣势对于简单的数组合并Stream API会带来额外的开销创建流对象、迭代器、可能的装箱/拆箱。IntStream处理基本类型时性能尚可但StreamT处理对象时其性能通常低于直接使用System.arraycopy()。注意点Stream.toArray()方法需要传入一个IntFunctionT[]来生成正确类型的数组通常使用T[]::new这种形式的方法引用如String[]::new。4. 性能对比与基准测试说一千道一万不如跑个分。我们使用JMHJava Microbenchmark Harness来对上述几种方案进行一个简单的性能对比。测试环境合并两个长度为1000的int[]数组。// 基准测试代码概要需引入JMH依赖 State(Scope.Thread) BenchmarkMode(Mode.AverageTime) OutputTimeUnit(TimeUnit.NANOSECONDS) public class ArrayMergeBenchmark { private int[] array1; private int[] array2; Setup public void setup() { array1 IntStream.range(0, 1000).toArray(); array2 IntStream.range(1000, 2000).toArray(); } Benchmark public int[] manualLoopMerge() { int[] result new int[2000]; for (int i 0; i 1000; i) result[i] array1[i]; for (int i 0; i 1000; i) result[1000 i] array2[i]; return result; } Benchmark public int[] systemArrayCopyMerge() { int[] result new int[2000]; System.arraycopy(array1, 0, result, 0, 1000); System.arraycopy(array2, 0, result, 1000, 1000); return result; } Benchmark public int[] streamMerge() { return IntStream.concat(Arrays.stream(array1), Arrays.stream(array2)).toArray(); } }预期的结果排序从快到慢System.arraycopy()毫无悬念的冠军。JVM级别的内存块复制开销最小。手动循环JIT编译器会对简单的循环进行优化如循环展开性能不错但依然有每次迭代的边界检查开销。Stream API最慢。它包含了流构建、迭代器、终端操作等一系列抽象层的开销。但在数据量不大或需要复杂处理时其带来的代码清晰度提升可以弥补性能损失。重要提示性能差异在数据量小比如几十个元素时微乎其微可忽略不计。选择方案的首要依据是代码清晰度和场景匹配度而非微小的性能差异。只有在处理真正的大数据如数万、百万级别时System.arraycopy()的优势才会变得至关重要。5. 边界情况与异常处理实战任何健壮的程序都必须考虑边界情况。数组合并看似简单但陷阱不少。5.1 空数组与null值处理这是最常见的坑。你的合并方法必须能优雅地处理以下情况array1为null,array2正常array1正常,array2为null两者都为null两者都为空数组长度为0处理策略建议防御性编程在方法开始处检查参数。定义清晰的行为例如约定“如果输入为null则视为空数组”。或者更严格地直接抛出IllegalArgumentException让调用者保证传入有效参数。前者更宽容后者更严格取决于你的API设计哲学。利用工具方法Arrays.copyOf()传入空数组或长度为0的数组是安全的它会返回一个同样为空的新数组。public static T T[] safeMerge(T[] first, T[] second) { // 策略将null视为空数组 first first null ? (T[]) new Object[0] : first; second second null ? (T[]) new Object[0] : second; T[] result Arrays.copyOf(first, first.length second.length); System.arraycopy(second, 0, result, first.length, second.length); return result; }注意上面的泛型空数组创建(T[]) new Object[0]在运行时是Object[]如果后续代码严格依赖组件类型可能会有ClassCastException风险。更安全的方式是使用Arrays.copyOf并指定类型或使用Collections.emptyList()的思路返回不可变空集合。5.2 数组合并后的去重问题如果需求是合并并去重那么“合并”本身只是第一步。常见做法使用Set无序去重将两个数组的所有元素添加到一个HashSet中然后转换回数组。时间复杂度接近O(n)但丢失了原顺序。SetT set new HashSet(Arrays.asList(array1)); set.addAll(Arrays.asList(array2)); T[] uniqueArray set.toArray(new T[0]); // 注意toArray的用法使用LinkedHashSet保持插入顺序去重在HashSet的基础上能保持元素第一次被添加时的顺序。使用Stream API推荐如方案四所示Stream.concat().distinct().toArray()代码简洁顺序取决于流中元素的相遇顺序通常是第一个数组在前。先合并再排序去重如果最终需要排序可以先合并然后用Arrays.sort()排序再遍历一遍移除相邻重复项。这与树状数组等高级数据结构无关是基础算法。5.3 内存溢出OutOfMemoryError合并两个超大数组时最直接的风险就是创建的新数组大小超出JVM堆内存限制。例如合并两个各占1GB内存的数组需要至少2GB的连续堆空间。预判在合并前可以粗略估算新数组大小。对于对象数组估算更复杂因为对象本身占用额外内存。策略如果数据量极大考虑是否真的需要一次性合并到内存中是否可以分批处理或者使用磁盘、数据库作为中间存储这时问题就从“数组合并”上升为“大数据处理”了。错误处理new数组时如果内存不足JVM会抛出OutOfMemoryError。对于可控的大数据操作应在操作前检查可用内存或使用更稳健的数据结构如ArrayList它内部是动态扩容的数组但合并时同样面临整体拷贝的问题。6. 举一反三从数组合并到相关场景掌握了基础数组合并很多看似复杂的问题都能迎刃而解。合并多个数组思路一致先计算总长度然后循环或多次调用System.arraycopy。public static int[] mergeMultipleArrays(int[]... arrays) { int totalLength 0; for (int[] arr : arrays) totalLength arr.length; int[] result new int[totalLength]; int destPos 0; for (int[] arr : arrays) { System.arraycopy(arr, 0, result, destPos, arr.length); destPos arr.length; } return result; }与集合互转List转数组用list.toArray()数组合并后转List用Arrays.asList(mergedArray)返回的是固定大小的列表。注意泛型数组的转换。“二维数组”的合并二维数组就是“数组的数组”。合并两个int[][]可以理解为合并两个“一维的数组引用”每个引用指向一个一维数组。方法同上但要注意深拷贝与浅拷贝的问题。面试题拓展很多java面试问题大全及答案大全中的题目如“合并两个有序链表/数组”、“寻找两个数组的交集/并集”其核心操作都包含遍历和元素搬运与数组合并的基本功一脉相承。7. 总结与最佳实践建议回顾这趟从基础到深入的旅程我们可以提炼出关于Java数组合并的几点核心建议首选System.arraycopy()对于绝大多数需要高性能、明确类型的数组合并场景无论是基本类型还是对象数组System.arraycopy()都是最可靠、最快速的选择。记住它的参数签名(src, srcPos, dest, destPos, length)。善用Arrays.copyOf()进行优雅初始化当需要以某个现有数组为模板创建更长的新数组时Arrays.copyOf()比手动new再复制更简洁安全尤其适用于泛型数组的创建。Stream API 用于声明式复杂操作当合并只是数据处理流水线中的一个环节并且后续还需要过滤、映射、去重、排序时Stream API能让你的代码意图更清晰。在性能不是绝对瓶颈的业务代码中这是提高可读性的利器。永远进行边界检查null和空数组是代码的“暗礁”。在方法入口处用条件语句明确处理它们能让你的方法更健壮避免NullPointerException。理解浅拷贝与深拷贝合并对象数组时你复制的是引用。如果修改新数组中的对象原数组中的对应对象也会改变。如果这不是你想要的你需要实现深拷贝这通常意味着要遍历数组并创建每个对象的新实例。性能权衡的艺术在数据量小1000时各种方法差异极小选择最清晰、最易维护的。在数据量大时优先考虑System.arraycopy()。对于超大数据需要重新评估架构考虑分治、流式处理或使用专门的大数据框架。最后看似简单的“数组合并”其实是检验一个Java程序员对基础数据结构、API库、内存模型和性能意识理解程度的试金石。下次当你写下合并数组的代码时不妨多思考一下背后的选择这会让你的代码从“能用”进化到“优秀”。