How to Merge Arrays in Java: Methods, Examples & Best Practices
Last verified: April 2026
Executive Summary
Merging arrays is one of the most fundamental operations in Java development, yet developers often reach for inefficient solutions without considering the standard library alternatives. Whether you’re combining two integer arrays, merging sorted collections, or concatenating object arrays, Java provides multiple built-in approaches that handle edge cases automatically—if you know where to look.
Learn Java on Udemy
This guide covers five practical methods to merge arrays in Java, from simple two-array concatenation to handling null values and empty inputs. We’ll walk through real production-ready code, explain the performance implications of each approach, and highlight the common mistakes that trip up developers. By the end, you’ll understand not just how to merge arrays, but why certain methods work better than others in specific situations.
Main Data Table: Array Merging Methods Comparison
| Method | Time Complexity | Space Complexity | Best Use Case | Handles Nulls |
|---|---|---|---|---|
| System.arraycopy() | O(n + m) | O(n + m) | High-performance, primitive arrays | No |
| Arrays.copyOf() with loops | O(n + m) | O(n + m) | Type-safe object arrays | Yes (via manual checks) |
| Stream API (concat) | O(n + m) | O(n + m) | Functional style, readability | Yes |
| Apache Commons Lang ArrayUtils | O(n + m) | O(n + m) | Complex scenarios, utility operations | Yes |
| ArrayList with addAll() | O(n + m) | O(n + m) | Dynamic size needs, frequent additions | Yes |
Breakdown by Experience Level & Use Cases
Different developers gravitate toward different methods based on their familiarity with Java’s ecosystem. Here’s what works for each experience level:
| Experience Level | Recommended Method | Reason | Learning Value |
|---|---|---|---|
| Beginner | ArrayList + addAll() | Intuitive, forgiving with edge cases | Introduces collections framework |
| Intermediate | System.arraycopy() or Streams | Balances performance and readability | Understanding memory and functional patterns |
| Advanced | System.arraycopy() for performance-critical code | Near-native speed, minimal overhead | Memory layout, JIT optimization awareness |
| Enterprise | Apache Commons or custom utility classes | Consistency, maintainability across teams | Code reuse, testing patterns |
5 Methods to Merge Arrays in Java (with Code Examples)
Method 1: System.arraycopy() — The Performance Champion
This is the fastest way to merge arrays in Java. System.arraycopy() is a native method that copies data at near-memory-speed with zero overhead. Use this when performance matters and you’re working with primitives or you’ve already validated your inputs.
public static int[] mergeArrays(int[] arr1, int[] arr2) {
int[] result = new int[arr1.length + arr2.length];
System.arraycopy(arr1, 0, result, 0, arr1.length);
System.arraycopy(arr2, 0, result, arr1.length, arr2.length);
return result;
}
// Usage
int[] array1 = {1, 2, 3};
int[] array2 = {4, 5, 6};
int[] merged = mergeArrays(array1, array2);
// Result: [1, 2, 3, 4, 5, 6]
Why this works: System.arraycopy() directly manipulates memory, bypassing Java’s object overhead. It’s ideal for primitive arrays or when you’ve already validated inputs aren’t null.
Pitfall: Doesn’t check for null values. Add guards if inputs are untrusted.
Method 2: Arrays.copyOf() with Manual Concatenation
For object arrays or when you want type safety with null-checking, this approach is more readable than System.arraycopy().
public static <T> T[] mergeArrays(T[] arr1, T[] arr2) {
if (arr1 == null || arr2 == null) {
throw new IllegalArgumentException("Arrays cannot be null");
}
T[] result = Arrays.copyOf(arr1, arr1.length + arr2.length);
System.arraycopy(arr2, 0, result, arr1.length, arr2.length);
return result;
}
// Usage with String arrays
String[] names1 = {"Alice", "Bob"};
String[] names2 = {"Charlie", "Diana"};
String[] allNames = mergeArrays(names1, names2);
// Result: ["Alice", "Bob", "Charlie", "Diana"]
Why this works: Generic type parameter preserves the array type, and we validate nulls upfront. Arrays.copyOf() internally uses System.arraycopy() anyway, so performance is nearly identical.
Pitfall: Type erasure means you lose generic info at runtime. For heterogeneous types, use Object[].
Method 3: Stream API with Arrays.stream()
For functional programming enthusiasts or when you want to chain operations, the Stream API provides elegant syntax.
public static int[] mergeArrays(int[] arr1, int[] arr2) {
return Stream.concat(
Arrays.stream(arr1),
Arrays.stream(arr2)
).toArray();
}
// Usage
int[] array1 = {10, 20};
int[] array2 = {30, 40};
int[] merged = mergeArrays(array1, array2);
// Result: [10, 20, 30, 40]
// With filtering (bonus: only even numbers)
public static int[] mergeAndFilter(int[] arr1, int[] arr2) {
return Stream.concat(
Arrays.stream(arr1),
Arrays.stream(arr2)
)
.filter(n -> n % 2 == 0)
.toArray();
}
Why this works: Streams are composable. You can add filter(), map(), or sort() operations without rewriting the merge logic.
Pitfall: Streams create intermediate objects. For massive arrays (millions of elements), use System.arraycopy() instead. Performance degrades when chaining multiple operations.
Method 4: ArrayList (Most Flexible)
When you don’t know the final size upfront or need to handle dynamic inputs, ArrayList is your friend.
public static Integer[] mergeArrays(Integer[] arr1, Integer[] arr2) {
List<Integer> list = new ArrayList<>(arr1.length + arr2.length);
list.addAll(Arrays.asList(arr1));
list.addAll(Arrays.asList(arr2));
return list.toArray(new Integer[0]);
}
// Safer version with null handling
public static Integer[] mergeArraysSafe(Integer[] arr1, Integer[] arr2) {
List<Integer> list = new ArrayList<>();
if (arr1 != null) {
list.addAll(Arrays.asList(arr1));
}
if (arr2 != null) {
list.addAll(Arrays.asList(arr2));
}
return list.toArray(new Integer[0]);
}
// Usage
Integer[] nums1 = {1, 2, 3};
Integer[] nums2 = {4, 5};
Integer[] merged = mergeArraysSafe(nums1, nums2);
// Result: [1, 2, 3, 4, 5]
Why this works: ArrayList handles resizing automatically and includes null safety by default. The toArray(new Integer[0]) pattern is faster than new Integer[list.size()] (JVM optimizes empty-array allocation).
Pitfall: Converting back to arrays loses the flexibility of lists. If you’re going to call merge repeatedly, keep data in lists.
Method 5: Apache Commons Lang (Production Code)
For enterprise codebases, external libraries like Apache Commons Lang are battle-tested and maintained.
// Add dependency: org.apache.commons:commons-lang3:3.14.0
import org.apache.commons.lang3.ArrayUtils;
public class ArrayMerger {
public static void main(String[] args) {
int[] arr1 = {1, 2, 3};
int[] arr2 = {4, 5, 6};
int[] merged = ArrayUtils.addAll(arr1, arr2);
System.out.println(Arrays.toString(merged));
// Output: [1, 2, 3, 4, 5, 6]
// Also works with objects
String[] names1 = {"Alice"};
String[] names2 = {"Bob", "Charlie"};
String[] allNames = ArrayUtils.addAll(names1, names2);
}
}
Why this works: ArrayUtils.addAll() handles all edge cases (nulls, empty arrays, different types) and is optimized for common cases. You’re letting experts handle complexity.
Pitfall: Adds a dependency. For small codebases, use System.arraycopy() instead.
Comparison: Merge Arrays vs. Related Operations
| Operation | Use Case | Recommended Method | Complexity |
|---|---|---|---|
| Merge two arrays | Combine fixed-size collections | System.arraycopy() | O(n + m) |
| Merge multiple arrays (3+) | Combine variable number of arrays | Stream.concat() or ArrayList | O(n1 + n2 + … + nk) |
| Merge and sort | Combined sorted result | Stream + sorted() | O((n + m) log(n + m)) |
| Merge and deduplicate | Union without duplicates | Stream + distinct() | O(n + m) |
| Merge removing nulls | Clean data pipeline | Stream + filter() | O(n + m) |
| Merge into existing array | In-place operations | System.arraycopy() (if space exists) | O(n + m) |
5 Key Factors Affecting Array Merge Performance
1. Array Size Matters More Than You’d Think
Merging two 1-million-element arrays with Streams creates overhead that System.arraycopy() avoids entirely. For production code handling large datasets, benchmark your approach. A 2-3x performance difference between methods is common with massive arrays.
2. Null Handling Is Not Free
Every null check adds a conditional branch. If your inputs are guaranteed non-null (checked at entry points), skip validation. If they might be null, use ArrayList or Streams—the safety is worth the minor overhead.
3. Object vs. Primitive Arrays Behave Differently
Primitive arrays (int[], double[]) are stored contiguously in memory. System.arraycopy() on primitives is pure memory copying. Object arrays store references; the merge still copies references, but you’re paying for pointer indirection. This is one reason why Collections often outperform raw arrays for object types.
4. JIT Compilation Affects Real-World Performance
Micro-benchmarks can be misleading. The JIT compiler hot-spots your code and optimizes it differently depending on usage patterns. System.arraycopy() is recognized by the JIT and may be inlined or optimized further. Run JMH (Java Microbenchmark Harness) benchmarks, not simple timing tests.
5. Memory Allocation Overhead Dominates Small Merges
For merging two 10-element arrays, allocation time dwarfs copying time. Method choice barely matters. For millions of elements, allocation is negligible—now method choice is critical. Scale your optimization accordingly.
Historical Trends: How Array Merging Has Evolved
Java’s approach to array operations has shifted significantly over the past decade:
- Java 8 (2014): Streams API introduced functional alternatives to imperative loops. Developers suddenly had choices.
- Java 9 (2017): VarHandle API gave lower-level control over array operations, but System.arraycopy() remained the performance standard.
- Java 11+ (2018-2021): Stream operations matured; Graal VM compiler improved optimization for functional code.
- Java 16+ (2021): Records and sealed classes changed data design patterns, but array merging mechanics stayed stable.
- Java 21+ (2023): Virtual threads and structured concurrency opened possibilities for parallel array merging (rarely beneficial for this operation).
The big surprise: System.arraycopy() remains undefeated for raw performance. Despite two decades of JVM improvements, nothing beats a direct memory copy for the use case of merging arrays.
Expert Tips for Production Code
Tip 1: Create a Utility Method, Not Inline Code
Avoid scattering array merges throughout your codebase. Create a single ArrayUtil class:
public final class ArrayUtil {
private ArrayUtil() {} // Utility class, no instantiation
public static <T> T[] merge(T[] first, T[] second, Class<T> type) {
if (first == null || second == null) {
throw new IllegalArgumentException("Input arrays cannot be null");
}
T[] result = Array.newInstance(type, first.length + second.length);
System.arraycopy(first, 0, result, 0, first.length);
System.arraycopy(second, 0, result, first.length, second.length);
return result;
}
}
Tip 2: Test Edge Cases Explicitly
Write unit tests for empty arrays, single-element arrays, and null inputs. This is where bugs hide:
@Test
void testMergeEmptyArrays() {
int[] empty1 = {};
int[] empty2 = {};
int[] result = mergeArrays(empty1, empty2);
assertEquals(0, result.length);
}
@Test
void testMergeOneEmpty() {
int[] arr = {1, 2, 3};
int[] empty = {};
int[] result = mergeArrays(arr, empty);
assertArrayEquals(new int[]{1, 2, 3}, result);
}
@Test
void testMergeWithNull() {
assertThrows(IllegalArgumentException.class,
() -> mergeArrays(null, new int[]{1, 2}));
}
Tip 3: Use the Right Tool for Each Scenario
Don’t use Streams for simple merges—it’s overkill. Don’t use System.arraycopy() when you need null safety—ArrayList is cleaner. Match the tool to the problem.
Tip 4: Document Performance Assumptions
If you chose System.arraycopy() for performance, add a comment explaining why. Future developers won’t refactor it to Streams if they understand the reasoning.
Tip 5: Benchmark Before Optimizing
Don’t assume System.arraycopy() is faster in your specific scenario. Use JMH to measure:
// Quick micro-benchmark (not professional-grade)
long start = System.nanoTime();
for (int i = 0; i < 100_000; i++) {
mergeArrays(arr1, arr2);
}
long duration = System.nanoTime() - start;
System.out.println("Time per call: " + (duration / 100_000.0) + " ns");
Frequently Asked Questions
Q1: Which method is fastest for merging two large integer arrays?
A: System.arraycopy() is the fastest. For two 1-million-element arrays on modern hardware, it executes in microseconds. Streams API is 3-5x slower due to stream pipeline overhead. However, the difference only matters if you're merging arrays millions of times per second. For most applications, the performance difference is imperceptible to users—optimize for readability instead.
Q2: Can I merge arrays without creating a new array?
A: Not in standard Java with fixed-size arrays. Arrays have fixed length at creation. You have three options: (1) Use an ArrayList—it's resizable. (2) Pre-allocate a larger array and use System.arraycopy() to fill it partially. (3) Use a custom data structure that wraps multiple arrays (rarely necessary). In 99% of cases, creating a new array is the right choice—it's safe and idiomatic.
Q3: How do I merge sorted arrays and keep them sorted?
A: Use Streams with sorted() or implement a merge-sort-like algorithm. The Streams approach is simplest:
int[] merged = Stream.concat(
Arrays.stream(arr1),
Arrays.stream(arr2)
)
.sorted()
.toArray();
If both input arrays are already sorted, a custom two-pointer merge is O(n + m) instead of O((n + m) log(n + m)):
public static int[] mergeSorted(int[] arr1, int[] arr2) {
int[] result = new int[arr1.length + arr2.length];
int i = 0, j = 0, k = 0;
while (i < arr1.length && j < arr2.length) {
result[k++] = arr1[i] <= arr2[j] ? arr1[i++] : arr2[j++];
}
while (i < arr1.length) result[k++] = arr1[i++];
while (j < arr2.length) result[k++] = arr2[j++];
return result;
}
Q4: What's the difference between Arrays.copyOf() and System.arraycopy()?
A: Arrays.copyOf() creates a new array of specified size and copies elements. System.arraycopy() copies elements between existing arrays. In practice, Arrays.copyOf() is implemented using System.arraycopy() internally, so performance is identical. Use Arrays.copyOf() when you want a cleaner API; use System.arraycopy() when you're already managing multiple arrays and want explicit control.
Q5: How do I merge arrays with null safety in production code?
A: Use ArrayList or validate inputs upfront. The ArrayList approach is safer:
public static <T> List<T> mergeSafe(T[] arr1, T[] arr2) {
List<T> result = new ArrayList<>();
if (arr1 != null) result.addAll(Arrays.asList(arr1));
if (arr2 != null) result.addAll(Arrays.asList(arr2));
return result;
}
// Returns List, not array—caller decides if conversion is needed
Alternatively, validate at the entry point and throw exceptions:
public static <T> T[] merge(T[] arr1, T[] arr2, Class<T> type) {
Objects.requireNonNull(arr1, "arr1 cannot be null");
Objects.requireNonNull(arr2, "arr2 cannot be null");
// ... proceed with merge
}
Conclusion: Choose the Right Method for Your Context
Merging arrays in Java has no one-size-fits-all solution. Your choice depends on three factors:
- Performance requirements: System.arraycopy() for high-throughput scenarios; Streams for anything else.
- Null safety: ArrayList for untrusted inputs; System.arraycopy() for validated data.
- Code maintainability: Streams for readability in functional pipelines; utility methods for consistency across codebases.
For most applications, start with ArrayList—it's safe, readable, and performant enough. Move to System.arraycopy() only when profiling shows it's a bottleneck. And always test edge cases: empty arrays, single elements, and null inputs are where bugs live.
The common mistakes listed at the start (ignoring edge cases, forgetting null checks, using inefficient algorithms) stem from treating array merging as trivial. It's not. Take 10 minutes to implement it correctly once, place it in a utility class, test it thoroughly, and reuse it everywhere. That's how production code stays maintainable.
Learn Java on Udemy
Related tool: Try our free calculator