Java Arrays: Declaration, Operations, and Pitfalls You Need to Know

Java arrays are fixed-size, indexed containers with O(1) random access. Full guide to declaring, iterating, copying, and searching them, plus the pitfalls that catch almost everyone.

Java Arrays: Declaration, Operations, and Pitfalls You Need to Know

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 length field
  • 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:

TypeDefault
int0
double0.0
booleanfalse
char'\u0000' (null character)
long0L
float0.0f
byte0
short0
Objectsnull
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

FeatureArray (int[])ArrayList (ArrayList<Integer>)
SizeFixed at creationDynamic - grows and shrinks
TypePrimitives or objectsObjects only (autoboxing for primitives)
PerformanceFaster - no boxing overheadSlightly slower - boxing/unboxing
MemoryLess overheadMore - stores wrapper objects + internal array
Random accessO(1)O(1)
Adding/removingManual - create new arrayO(1) amortized (add at end)
Use whenSize is known and fixedSize 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:

  1. Performance-critical code: no boxing overhead, direct memory access
  2. Multi-dimensional data: int[][] is cleaner and faster than ArrayList<ArrayList<Integer>>
  3. Interfacing with APIs: many Java APIs return or accept arrays (String[] args, Thread constructors, NIO buffers)
  4. Fixed-size data: if the size truly never changes, an array communicates that intent
  5. Primitive collections: When you need int performance 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.length gives you rows or columns. It gives you rows (the outer array’s length). The number of columns is matrix[0].length: but be careful with jagged arrays where rows have different lengths.

Member discussion

0 comments

Start the conversation

Become a member of >hacksubset_ to start commenting.