Java Arrays

An array in Java is a fixed-size, ordered collection of elements that all share the same type and live together in a single block of memory. Arrays let you group related values — a list of scores, a grid of pixels, a batch of sensor readings — under one variable name, and access any element instantly by its numeric index. They are the simplest and fastest data structure Java offers, and they form the foundation that more flexible collections like ArrayList are built on top of. This lesson covers arrays from first principles all the way through memory internals, multidimensional arrays, the Arrays utility class, and the mistakes that trip up almost every new Java programmer.

Overview: How Arrays Work

Every array in Java has three defining properties: a fixed length chosen when the array is created, a single element type shared by every slot (int, String, boolean, another array, etc.), and zero-based indexing, meaning the first element is at index 0 and the last is at index length - 1. Once an array is created, its length can never change — there is no way to add or remove a slot. If you need more room, you must create a brand-new, larger array and copy the existing elements into it (the Arrays.copyOf method, shown below, does exactly this).

Arrays Are Objects

Even an array of primitives such as int[] is an object in Java, allocated on the heap, not the stack. The variable you declare (for example int[] numbers) is a reference to that heap object, exactly like a reference to any other object. This has real consequences:

  • Arrays expose their size through a public length field, not a method — there are no parentheses after it, unlike String.length().
  • Assigning one array variable to another copies the reference, not the data. Both variables end up pointing at the same underlying array, so changes made through one are visible through the other.
  • When you pass an array to a method, the method receives a copy of the reference, but that reference points at the same array, so modifying elements inside the method is visible to the caller after the method returns.
  • Because arrays are objects, an array reference can also be null, meaning it doesn’t point at an array at all — reading .length or an element from a null array throws a NullPointerException.

Default Values

When you create an array with new, Java automatically fills every slot with a default value based on the element type — you never get leftover “garbage” memory. Numeric types default to 0 (or 0.0 for floating-point types), boolean defaults to false, char defaults to the null character, and any reference type (String, custom objects, other arrays) defaults to null. This differs from local variables, which the compiler forces you to initialize before use.

Syntax

An array declaration has two parts: the element type followed by square brackets, and a name. You can declare, instantiate, and initialize an array separately or all at once.

Form Example Meaning
Declaration int[] arr; Declares a reference variable; no array exists yet (value is null).
Instantiation arr = new int[5]; Creates an array of 5 ints, all initialized to 0, and assigns it to arr.
Combined int[] arr = new int[5]; Declares and instantiates in one statement.
Array literal int[] arr = {1, 2, 3}; Declares and fills with known values; length is inferred (3).
Explicit literal int[] arr = new int[]{1, 2, 3}; Same as above, but usable outside a declaration (e.g. as a method argument).

The square brackets can also legally appear after the variable name (int arr[]), a holdover from C-style syntax — this compiles, but placing the brackets after the type (int[] arr) is the idiomatic and recommended style in Java.

int[] arrayName;
arrayName = new int[5];

int[] arrayName2 = new int[5];
int[] arrayName3 = {1, 2, 3, 4, 5};
int[] arrayName4 = new int[]{1, 2, 3};

System.out.println("arrayName2 length: " + arrayName2.length);
System.out.println("arrayName3 length: " + arrayName3.length);
System.out.println("arrayName4 length: " + arrayName4.length);
Output:
arrayName2 length: 5
arrayName3 length: 5
arrayName4 length: 3

Examples

Example 1: Declaring, Filling, and Reading a 1D Array

public class Main {
    public static void main(String[] args) {
        int[] scores = {85, 92, 78, 90, 65};

        int sum = 0;
        for (int i = 0; i < scores.length; i++) {
            System.out.println("Score " + i + ": " + scores[i]);
            sum += scores[i];
        }
        double average = (double) sum / scores.length;
        System.out.println("Average: " + average);

        System.out.print("All scores:");
        for (int score : scores) {
            System.out.print(" " + score);
        }
        System.out.println();
    }
}
Output:
Score 0: 85
Score 1: 92
Score 2: 78
Score 3: 90
Score 4: 65
Average: 82.0
All scores: 85 92 78 90 65

The first loop uses an indexed for loop because it needs both the index i and the value scores[i]. The second loop uses the enhanced for loop (for (int score : scores)), which reads more cleanly when you only need each value, not its position. Notice the cast (double) sum — without it, sum / scores.length would perform integer division and truncate the result.

Example 2: Two-Dimensional Arrays

public class Main {
    public static void main(String[] args) {
        int[][] matrix = {
            {1, 2, 3},
            {4, 5, 6},
            {7, 8, 9}
        };

        int total = 0;
        for (int row = 0; row < matrix.length; row++) {
            for (int col = 0; col < matrix[row].length; col++) {
                System.out.print(matrix[row][col] + " ");
                total += matrix[row][col];
            }
            System.out.println();
        }
        System.out.println("Sum of all elements: " + total);
    }
}
Output:
1 2 3 
4 5 6 
7 8 9 
Sum of all elements: 45

Java doesn’t have “true” multidimensional arrays the way some languages do — int[][] matrix is really an array of arrays: matrix is an array of three int[] references, and each of those references points to its own independent int[3]. That’s why the inner loop bound is written as matrix[row].length rather than a fixed number — each row could, in principle, have a different length (a “jagged” array), even though in this example all three rows happen to have three columns.

Example 3: The Arrays Utility Class

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] numbers = {42, 17, 8, 99, 23};
        System.out.println("Original: " + Arrays.toString(numbers));

        Arrays.sort(numbers);
        System.out.println("Sorted: " + Arrays.toString(numbers));

        int index = Arrays.binarySearch(numbers, 42);
        System.out.println("Index of 42: " + index);

        int[] expanded = Arrays.copyOf(numbers, 7);
        System.out.println("Expanded: " + Arrays.toString(expanded));

        int[] filled = new int[5];
        Arrays.fill(filled, 9);
        System.out.println("Filled: " + Arrays.toString(filled));
    }
}
Output:
Original: [42, 17, 8, 99, 23]
Sorted: [8, 17, 23, 42, 99]
Index of 42: 3
Expanded: [8, 17, 23, 42, 99, 0, 0]
Filled: [9, 9, 9, 9, 9]

Printing an array directly with System.out.println(numbers) does not print its contents — it prints something like [I@1b6d3586, a type code plus a hash. Always use Arrays.toString() (or Arrays.deepToString() for nested arrays) to print readable contents. This example also shows why sorting matters: Arrays.binarySearch only works correctly on a sorted array — running it on an unsorted array gives an undefined result. Arrays.copyOf is the standard way to “grow” an array: it allocates a new array of the requested length, copies over the old elements, and pads any new slots with the default value (0 here).

Under the Hood: What the JVM Does

Conceptually, an array is stored as one contiguous block of memory on the heap, preceded by a small header that records its type and length. Reading arr.length is effectively free — the JVM does not count the elements each time, it just returns the stored value. Accessing arr[i] is (conceptually) computed as base_address + i * element_size, which is why array access is O(1) regardless of length or index.

Walking through int[] arr = new int[5]; followed by arr[2] = 10;:

  1. The JVM allocates a contiguous block on the heap large enough for five int values, plus a header storing the type (int[]) and length (5).
  2. Every slot is initialized to the default value for int, which is 0.
  3. The reference to that heap block is stored in the local variable arr.
  4. For arr[2] = 10;, the JVM checks that 2 is within the valid range [0, 5), then writes 10 at the corresponding offset.
  5. If the index were out of range — say arr[10] — the JVM refuses to touch memory outside the block and throws ArrayIndexOutOfBoundsException instead. This bounds check runs on every array access, which is what makes Java memory-safe compared to languages like C that let you read and write past the end of an array.

One more subtlety applies to arrays of objects. Because of a feature called array covariance, a String[] can be assigned to an Object[] variable, since every String is an Object. But the array still remembers its real, original component type at runtime. If you then try to store something other than a String through that Object[] reference, the JVM detects the mismatch at runtime and throws ArrayStoreException, even though the code compiled without complaint. This is a rare gotcha but worth knowing if you ever see that exception in a stack trace.

Common Mistakes

Mistake 1: Off-by-One Errors (ArrayIndexOutOfBoundsException)

The most common array bug is looping one step too far by using <= instead of < against .length. Valid indices run from 0 to length - 1, so arr[arr.length] is always one past the end.

public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        for (int i = 0; i <= arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}
Output:
1
2
3
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at Main.main(Main.java:5)

The fix is simply to use < instead of <=:

public class Main {
    public static void main(String[] args) {
        int[] arr = {1, 2, 3};
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
}
Output:
1
2
3

Mistake 2: Calling .length() Instead of Using .length

Arrays expose their size as a field called length, with no parentheses. This is easy to mix up with String.length(), which is a method. Writing arr.length() on an array is a compile-time error, not a runtime one:

int[] arr = {1, 2, 3};
System.out.println(arr.length()); // compile error: cannot find symbol -- length() is not a method on arrays

The fix is to drop the parentheses:

int[] arr = {1, 2, 3};
System.out.println("Length: " + arr.length);
Output:
Length: 3

A related mistake is assuming an array can grow, e.g. expecting arr[arr.length] = x; to “append” a value. Arrays never resize themselves — use Arrays.copyOf to build a larger array (as shown in Example 3), or switch to a java.util.ArrayList if the collection’s size needs to change at runtime.

Best Practices

  • Use the enhanced for loop (for (T x : array)) when you only need values, and the indexed loop when you need the position too.
  • Always bound loops with array.length rather than a hardcoded number, so the code keeps working if the array’s size changes.
  • Use java.util.Arrays helper methods — toString, sort, fill, equals, copyOf, binarySearch — instead of hand-rolling loops for common operations.
  • Validate indices (especially ones derived from user input) before using them, rather than catching ArrayIndexOutOfBoundsException after the fact.
  • Prefer array-literal syntax ({1, 2, 3}) when the initial values are known ahead of time — it’s shorter and clearer than assigning each slot individually.
  • Reach for ArrayList or another java.util collection when the number of elements will change at runtime; arrays are the right tool only when the size is fixed and known.
  • Avoid returning a class’s internal array directly from a getter — return a copy (e.g. via Arrays.copyOf) so callers can’t mutate your object’s internal state.

Practice Exercises

  1. Write a program that fills an int array of length 10 with the first ten even numbers (2, 4, 6, …, 20) and prints their sum.
  2. Write a program that uses Scanner to read five integers from the user into an array, then prints the largest and smallest values found.
  3. Write a program that builds a 3×3 int matrix where element [row][col] equals row * 3 + col, prints it, then prints its transpose (rows and columns swapped).

Summary

  • An array is a fixed-size, zero-indexed collection of elements of the same type, allocated as a single object on the heap.
  • Once created, an array’s length cannot change — use Arrays.copyOf or an ArrayList when you need dynamic sizing.
  • length is a field (no parentheses), not a method like String.length().
  • Multidimensional arrays in Java are arrays of arrays, which is why rows can have different lengths (jagged arrays).
  • Every array access is bounds-checked at runtime, throwing ArrayIndexOutOfBoundsException on invalid indices — this is what keeps Java memory-safe.
  • The java.util.Arrays class provides ready-made methods for printing, sorting, searching, filling, and copying arrays — prefer them over manual loops.