I was reviewing a junior dev’s PR last month and saw them use == to compare two arrays. It compiled, ran, and returned false every time, even when the arrays had identical contents. They’d been chasing a “bug” for an hour that wasn’t a bug at all.
Arrays are the oldest data structure in Java, and they’re still everywhere, from storing command-line arguments to buffering pixels on screen. Despite their simplicity, the gotchas are real and they’ll waste your time if you don’t know them.
What Is a Java Array?
An array is a fixed-size, indexed container of elements of the same type. Once created, its length never changes. Elements are stored in contiguous memory, giving you O(1) random access by index.
int[] numbers = {10, 20, 30, 40, 50};
Index: 0 1 2 3 4
┌────┬────┬────┬────┬────┐
│ 10 │ 20 │ 30 │ 40 │ 50 │
└────┴────┴────┴────┴────┘
Length: 5
Key facts:
- Arrays are objects in Java: they live on the heap and have a
lengthfield - Indexing starts at 0, ends at
length - 1 - The element type is fixed at creation time
- Arrays know their own length (unlike C, where you pass size separately)
Declaring and Initializing Arrays
There are several ways to create arrays, each with different trade-offs.
Declaration Syntax
// These are all equivalent declarations
int[] nums; // preferred - type belongs to the array
int nums[]; // valid but discouraged - looks like C syntax
int [] nums; // valid but unusual
Initialization Options
// Option 1: Declare then allocate
int[] nums;
nums = new int[5]; // [0, 0, 0, 0, 0] - defaults to 0
// Option 2: Declare and allocate together
int[] nums = new int[5]; // [0, 0, 0, 0, 0]
// Option 3: Literal initialization
int[] nums = {10, 20, 30}; // length = 3
// Option 4: New array with values (Java 9+)
int[] nums = new int[]{10, 20, 30};
// Option 5: Empty array
int[] empty = new int[0]; // length = 0, valid and useful
Default Values
Every primitive type gets a default when you allocate with new:
| Type | Default |
|---|---|
int | 0 |
double | 0.0 |
boolean | false |
char | '\u0000' (null character) |
long | 0L |
float | 0.0f |
byte | 0 |
short | 0 |
| Objects | null |
String[] names = new String[3];
System.out.println(names[0]); // null - not "", not undefined
The null default for objects trips people up. You’ll get a NullPointerException if you call a method on names[0] without assigning it first.
Accessing and Modifying Elements
int[] arr = {10, 20, 30, 40, 50};
// Read
int first = arr[0]; // 10
int last = arr[4]; // 50
// Write
arr[2] = 99; // arr is now {10, 20, 99, 40, 50}
// Length
int len = arr.length; // 5 (it's a field, not a method)
The ArrayIndexOutOfBoundsException
The most common array error. Accessing any index outside [0, length-1] throws it:
int[] arr = {1, 2, 3};
arr[3]; // ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
arr[-1]; // ArrayIndexOutOfBoundsException: Index -1 out of bounds for length 3
Always validate bounds when index comes from user input or computation:
if (index >= 0 && index < arr.length) {
// safe to access
}
I’ve been bitten by this in production exactly once. It was an off-by-one error in a pagination handler and it took down our API for 20 minutes. I never skip the bounds check anymore.
Iterating Over Arrays
Traditional for Loop
int[] arr = {10, 20, 30, 40, 50};
for (int i = 0; i < arr.length; i++) {
System.out.println("arr[" + i + "] = " + arr[i]);
}
Use this when you need the index, for modifying elements, comparing neighbors, or stepping through in non-sequential order.
Enhanced for Loop (for-each)
int[] arr = {10, 20, 30, 40, 50};
for (int num : arr) {
System.out.println(num);
}
Cleaner when you only need the values. You cannot modify the array or access the index.
While Loop
int[] arr = {10, 20, 30, 40, 50};
int i = 0;
while (i < arr.length) {
System.out.println(arr[i]);
i++;
}
Rarely needed for arrays, but useful when the termination condition is more complex than a simple index check.
Streams (Java 8+)
int[] arr = {10, 20, 30, 40, 50};
Arrays.stream(arr)
.filter(n -> n > 20)
.map(n -> n * 2)
.forEach(System.out::println); // 60, 80, 100
Common Array Operations
Searching
Linear Search: O(n), works on any array:
public static int linearSearch(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) return i;
}
return -1;
}
Binary Search: O(log n), requires a sorted array:
import java.util.Arrays;
int[] arr = {10, 20, 30, 40, 50};
int index = Arrays.binarySearch(arr, 30); // 2
int missing = Arrays.binarySearch(arr, 25); // -(insertion point) = -3
The negative return value for missing elements is one of those Java API quirks that’s worth remembering. It encodes where the element would be inserted, which is actually useful.
Sorting
import java.util.Arrays;
int[] arr = {50, 10, 40, 20, 30};
Arrays.sort(arr); // {10, 20, 30, 40, 50} - primitive: dual-pivot quicksort
Arrays.sort(arr, 1, 4); // sort only indices 1..3: {10, 20, 30, 40, 50}
String[] names = {"Charlie", "Alice", "Bob"};
Arrays.sort(names); // {"Alice", "Bob", "Charlie"} - String uses compareTo
Arrays.sort(names, Comparator.reverseOrder()); // {"Charlie", "Bob", "Alice"}
Copying
int[] original = {10, 20, 30, 40, 50};
// Option 1: Arrays.copyOf - creates new array, fills extra with default
int[] copy1 = Arrays.copyOf(original, 3); // {10, 20, 30}
int[] copy2 = Arrays.copyOf(original, 7); // {10, 20, 30, 40, 50, 0, 0}
// Option 2: Arrays.copyOfRange - subarray
int[] copy3 = Arrays.copyOfRange(original, 1, 4); // {20, 30, 40}
// Option 3: System.arraycopy - fast, copies into existing array
int[] dest = new int[5];
System.arraycopy(original, 0, dest, 0, 5);
// Option 4: clone
int[] copy4 = original.clone();
// WARNING: This does NOT copy - it aliases:
int[] alias = original; // both point to the same array
alias[0] = 99; // original[0] is now 99 too
The aliasing issue is what caught that junior dev. int[] b = a doesn’t copy the array, both variables point to the same object in memory.
Filling
int[] arr = new int[10];
Arrays.fill(arr, 42); // all elements become 42
Arrays.fill(arr, 2, 5, 99); // indices 2, 3, 4 become 99
Comparing
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
int[] c = {1, 2, 4};
Arrays.equals(a, b); // true - element-by-element comparison
Arrays.equals(a, c); // false
Don’t use ==: it compares references, not values:
a == b; // false - different objects
a == a; // true - same reference
Converting to String
int[] arr = {10, 20, 30};
System.out.println(Arrays.toString(arr)); // [10, 20, 30]
// Without Arrays.toString:
System.out.println(arr); // [I@15db9742 - useless hash code
If you’ve ever seen [I@15db9742 in your logs and wondered what happened, that’s the default toString() for an int array. Always use Arrays.toString().
Resizing (Creating a New Array)
Since arrays are fixed-size, you create a new one and copy:
int[] arr = {10, 20, 30};
int[] resized = Arrays.copyOf(arr, arr.length * 2); // double the size
// resized: {10, 20, 30, 0, 0, 0}
This is how ArrayList works internally, it doubles the backing array when it runs out of space.
Multi-Dimensional Arrays
Java supports arrays of arrays. The most common is 2D:
Declaration and Initialization
// 2D array: 3 rows, 4 columns
int[][] matrix = new int[3][4];
// Literal initialization
int[][] matrix = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// Jagged array - rows can have different lengths
int[][] jagged = new int[3][];
jagged[0] = new int[]{1, 2};
jagged[1] = new int[]{3, 4, 5};
jagged[2] = new int[]{6};
Accessing Elements
int[][] matrix = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
int val = matrix[1][2]; // 6 (row 1, column 2)
matrix[0][0] = 100; // modify
Iterating 2D Arrays
// Nested for loop
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
System.out.print(matrix[i][j] + " ");
}
System.out.println();
}
// For-each (simpler but no index access)
for (int[] row : matrix) {
for (int val : row) {
System.out.print(val + " ");
}
System.out.println();
}
Why Java Uses Arrays of Arrays
A 2D array in Java is actually an array of references to arrays:
matrix (int[][])
│
├── [0] ── int[] {1, 2, 3}
├── [1] ── int[] {4, 5, 6}
└── [2] ── int[] {7, 8, 9}
This is why jagged arrays work, each row can be a different length. It also means matrix[0] is a valid int[] reference on its own. (This is also why matrix.length gives you rows, not columns, a common interview gotcha.)
Common 2D Array Pattern: Transpose
public static int[][] transpose(int[][] matrix) {
int rows = matrix.length;
int cols = matrix[0].length;
int[][] result = new int[cols][rows];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
result[j][i] = matrix[i][j];
}
}
return result;
}
Array vs ArrayList: When to Use Which
| Feature | Array (int[]) | ArrayList (ArrayList<Integer>) |
|---|---|---|
| Size | Fixed at creation | Dynamic - grows and shrinks |
| Type | Primitives or objects | Objects only (autoboxing for primitives) |
| Performance | Faster - no boxing overhead | Slightly slower - boxing/unboxing |
| Memory | Less overhead | More - stores wrapper objects + internal array |
| Random access | O(1) | O(1) |
| Adding/removing | Manual - create new array | O(1) amortized (add at end) |
| Use when | Size is known and fixed | Size changes or unknown at compile time |
The Autoboxing Trap
int[] primitives = {1, 2, 3}; // 3 objects in memory (3 ints)
ArrayList<Integer> objects = new ArrayList<>();
objects.add(1); objects.add(2); objects.add(3); // 6+ objects (Integer wrappers + internal array)
// Performance difference adds up in tight loops
for (int i = 0; i < 1_000_000; i++) {
primitives[i] = i; // fast - direct memory write
objects.add(i); // slower - creates Integer object each time
}
Rule of thumb: Use arrays for performance-critical code with known sizes. Use ArrayList for everything else. (I default to ArrayList unless I have a reason not to.)
Arrays of Objects
Arrays aren’t limited to primitives. You can store any object:
String[] names = {"Alice", "Bob", "Charlie"};
System.out.println(names[0].length()); // 5 - String methods work directly
// Array of custom objects
record Person(String name, int age) {}
Person[] people = {
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 35)
};
// Sort by age
Arrays.sort(people, Comparator.comparingInt(Person::age));
Common Pitfalls
Pitfall 1: Array Assignment Copies the Reference
int[] a = {1, 2, 3};
int[] b = a; // b points to the SAME array
b[0] = 99;
System.out.println(a[0]); // 99 - a is modified too!
// Fix: use Arrays.copyOf() or System.arraycopy()
int[] c = Arrays.copyOf(a, a.length);
c[0] = 1;
System.out.println(a[0]); // 99 - a is unchanged
Pitfall 2: Comparing Arrays with ==
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
a == b; // false - different objects, different memory addresses
Arrays.equals(a, b); // true - element-by-element comparison
Pitfall 3: Forgetting Array is Zero-Indexed
int[] arr = {10, 20, 30};
// arr[3] throws ArrayIndexOutOfBoundsException
// Last element is arr[arr.length - 1]
Pitfall 4: Modifying an Array During Iteration
int[] arr = {1, 2, 3, 4, 5};
// WRONG: Skipping elements - after removing arr[1], arr[2] shifts to index 1
for (int i = 0; i < arr.length; i++) {
if (arr[i] == 2) {
// remove logic here would skip element at i+1
}
}
// CORRECT: Iterate backwards when removing
for (int i = arr.length - 1; i >= 0; i--) {
if (arr[i] == 2) {
// safe to remove - earlier indices are unaffected
}
}
Pitfall 5: ArrayStoreException
Object[] arr = new String[3];
arr[0] = "hello"; // OK - String IS-A Object
arr[1] = 42; // WRONG: ArrayStoreException - Integer IS NOT a String
The array remembers its actual element type at runtime, even when accessed through a parent reference. This one is subtle and you probably won’t hit it often, but when you do, it’s confusing if you don’t know about it.
Advanced: Arrays.toString() and Deep Printing
int[][] matrix = {{1, 2}, {3, 4}};
System.out.println(Arrays.toString(matrix)); // [[I@15db9742, [I@6d06d69c] - useless
System.out.println(Arrays.deepToString(matrix)); // [[1, 2], [3, 4]] - use this for nested arrays
Advanced: Arrays.fill and Bulk Operations
int[] arr = new int[100];
// Fill entire array
Arrays.fill(arr, -1); // all -1
// Fill a range
Arrays.fill(arr, 10, 20, 0); // indices 10..19 become 0
Advanced: Parallel Sort (Java 8+)
For large arrays, Arrays.parallelSort uses multiple threads:
int[] hugeArray = new int[10_000_000];
// ... fill with data ...
Arrays.sort(hugeArray); // single-threaded - fine for small arrays
Arrays.parallelSort(hugeArray); // multi-threaded - faster for large arrays
Benchmarking matters. For arrays under ~10,000 elements, sort is often faster due to thread overhead. I tested this once on a 5,000-element array and sort beat parallelSort by about 15%.
When Arrays Are Still the Right Choice
Despite ArrayList’s convenience, arrays win in specific scenarios:
- Performance-critical code: no boxing overhead, direct memory access
- Multi-dimensional data:
int[][]is cleaner and faster thanArrayList<ArrayList<Integer>> - Interfacing with APIs: many Java APIs return or accept arrays (
String[] args,Threadconstructors, NIO buffers) - Fixed-size data: if the size truly never changes, an array communicates that intent
- Primitive collections: When you need
intperformance without wrapper objects
What I Take Away
Arrays are the kind of thing you learn once and use forever. The syntax is simple, but the edge cases, aliasing, bounds checking, zero-indexing, == vs equals(), are where people waste time. Know them upfront and you’ll avoid the bugs that keep showing up in code reviews.
The one thing I’d tell anyone learning Java: arrays won’t teach you fancy algorithms, but every fancy algorithm in Java runs on top of them. Get the basics right and the rest follows.
Pro Tip: When working with 2D arrays in interviews, always clarify whether
matrix.lengthgives you rows or columns. It gives you rows (the outer array’s length). The number of columns ismatrix[0].length: but be careful with jagged arrays where rows have different lengths.
Member discussion
0 commentsStart the conversation
Become a member of >hacksubset_ to start commenting.
Already a member? Sign in