☕ Java

Array Basics

An array in Java is a fixed-size, ordered collection of elements of the same type stored in a contiguous block of memory. Once created, the size of an array cannot change. Arrays are the simplest and most fundamental data structure in Java — they underlie many higher-level collections and provide direct, indexed access to elements in constant time O(1). Every array in Java is an object, inherits from java.lang.Object, and can hold either primitives or references.

What an Array Is and How It Works in Memory

An array is a contiguous block of memory that holds a fixed number of values of a single type. "Contiguous" means the elements sit side by side in memory with no gaps between them. This layout is what makes arrays so efficient: to reach element at index i, the JVM computes the address as base_address + (i × element_size) and reads that location directly. This arithmetic is instantaneous regardless of the array size — O(1) access time — which is the defining performance characteristic of arrays. When you declare an array variable in Java, you are declaring a reference — a variable that holds the address of the array object on the heap, not the array data itself. The array object is created separately on the heap when you use new or an array initialiser. This means arrays in Java participate in garbage collection and are subject to the same reference semantics as all other Java objects. For primitive arrays (int[], double[], boolean[]), the array object contains the actual primitive values packed tightly in memory. A 1000-element int array occupies exactly 4000 bytes of data (plus object header overhead). For reference arrays (String[], Object[], custom class arrays), the array object contains references — 4 or 8 bytes each depending on the JVM — and the actual objects are stored elsewhere on the heap. This distinction matters for performance: accessing elements of a primitive array is a single memory read, while accessing elements of a reference array requires two reads — one to get the reference, one to follow it to the actual object. The fixed size of arrays is a consequence of the contiguous memory model. To resize an array, you must allocate a new block of memory and copy all elements, which is O(n). Java's ArrayList and other collection classes handle this automatically, but they do so by wrapping arrays internally — at the lowest level, all Java data structures that grow are backed by arrays that are periodically reallocated and copied.
Java
// ── Array variable is a reference — array data lives on the heap: ──────
int[] numbers;          // declares a reference variable — currently null
numbers = new int[5];   // creates the array on the heap — reference stored in numbers

// ── Memory layout (primitive int array of size 5): ────────────────────
// Stack:              Heap:
// numbers ──────────▶ [0][0][0][0][0]
//   (reference)        [0] [1] [2] [3] [4]  ← indices
//                      addresses: 1000, 1004, 1008, 1012, 1016
//                      (each int is 4 bytes — contiguous)

// ── Accessing element at index i: ────────────────────────────────────
// address = base_address + (i × 4)
// numbers[0] → 1000 + (0 × 4) = 1000  — direct memory access — O(1)
// numbers[3] → 1000 + (3 × 4) = 1012  — same speed regardless of index

// ── Reference array — stores references, not objects: ─────────────────
// Stack:              Heap:
// names ────────────▶ [ref0][ref1][ref2]
//                       │     │     │
//                       ▼     ▼     ▼
//                     "Alice" "Bob" "Carol"String objects elsewhere

String[] names = {"Alice", "Bob", "Carol"};
// names[0] holds a reference to "Alice", not "Alice" itself.
// Accessing names[0] requires two memory lookups:
//   1. Read names[0] to get the reference to the String object
//   2. Follow the reference to read the String data

// ── Arrays are objects — have a class, can be passed around: ──────────
int[]    arr  = new int[10];
Object   obj  = arr;            // array IS-A Object — upcasting works
Class<?> cls  = arr.getClass();
System.out.println(cls.getName());  // [I  (JVM name for int[])
System.out.println(arr instanceof Object);  // true

Declaring, Creating, and Initialising Arrays

Java provides three syntactically distinct ways to create an array, each suited to different situations. The first separates declaration from creation — declare the variable first, then use new to allocate the array at any point later, and then populate it element by element. This is needed when the size is not known at declaration time or when creation and population must happen in separate steps. The second combines declaration and creation — the new expression specifies the type and size, and the array is immediately allocated with all elements set to their type's default value (zero for numeric types, false for boolean, null for reference types). This is the right approach when you know the size but will fill the array through computed values or input reading. The third is the array initialiser syntax — a comma-separated list of values in curly braces that serves as both a size declaration and a population step simultaneously. The compiler counts the values and sets the size accordingly. This is the most concise and readable form and is preferred whenever the values are known at the point of declaration. The length property provides the number of elements. It is a final field on every array object, read with array.length (no parentheses — it is not a method call). This is a frequent source of confusion for programmers coming from other languages where length is a method, and for programmers who confuse it with String's length() method which does use parentheses. The length of an array never changes after creation — it reflects exactly the size specified when the array was created.
Java
// ── Three ways to create an array: ───────────────────────────────────

// 1. Declare then create and populate separately:
int[] scores;                   // declaration — reference is null
scores = new int[5];            // creation — allocates 5 ints, all 0
scores[0] = 90;                 // population — set individual elements
scores[1] = 85;
scores[2] = 92;
scores[3] = 78;
scores[4] = 88;

// 2. Declare and create together — populate later:
double[] prices = new double[4];  // all 0.0
prices[0] = 9.99;
prices[1] = 14.99;
prices[2] = 4.99;
prices[3] = 19.99;

// 3. Array initialiser — declare, create, and populate in one step:
String[] days = {"Monday", "Tuesday", "Wednesday",
                 "Thursday", "Friday", "Saturday", "Sunday"};
// Size is 7 — compiler counts the elements.

// ── Default values — elements start at type defaults: ─────────────────
int[]     ints    = new int[3];      // [0, 0, 0]
double[]  doubles = new double[3];   // [0.0, 0.0, 0.0]
boolean[] bools   = new boolean[3];  // [false, false, false]
char[]    chars   = new char[3];     // ['', '', '']
String[]  strs    = new String[3];   // [null, null, null]
Object[]  objs    = new Object[3];   // [null, null, null]

// ── length property — number of elements: ────────────────────────────
int[] arr = {10, 20, 30, 40, 50};
System.out.println(arr.length);      // 5
// arr.length — NOT arr.length() — it is a field, not a method

// ── Bracket placement — both styles compile, first preferred: ──────────
int[] preferred = new int[5];        // brackets after type — Java style
int   notPreferred[] = new int[5];   // brackets after name — C style

// ── Array of size zero — valid, sometimes useful: ─────────────────────
int[] empty = new int[0];
System.out.println(empty.length);    // 0
// Used for methods that return arrays when there are no results,
// rather than returning null (which would require null checks).

Accessing Elements and Bounds Checking

Array elements are accessed using the index operator []. Java arrays are zero-indexed — the first element is at index 0, the second at index 1, and the last element is at index length-1. This off-by-one relationship between length and the last valid index is the source of the most common array error: accessing index length when the valid range ends at length-1. Java performs automatic bounds checking on every array access. If you attempt to access an index that is negative or greater than or equal to the length, the JVM throws an ArrayIndexOutOfBoundsException at runtime. This is one of Java's safety features — unlike C and C++, which do not perform bounds checking and allow out-of-bounds accesses to corrupt memory silently. Java's bounds checking guarantees that an out-of-bounds access always produces a clear, catchable exception rather than undefined behaviour. The bounds check happens at runtime because the JVM cannot always determine at compile time whether an index will be in range — the index might come from user input, a computation, or a loop variable. The JIT compiler can sometimes eliminate bounds checks for simple loop patterns where it can prove statically that the index stays within range, but the language semantics always guarantee that an invalid access produces an exception.
Java
// ── Zero-based indexing: ─────────────────────────────────────────────
String[] fruits = {"Apple", "Banana", "Cherry", "Date", "Elderberry"};
//                   [0]      [1]       [2]       [3]       [4]
//                                                        length-1 = 4

System.out.println(fruits[0]);              // Apple   — first element
System.out.println(fruits[4]);              // Elderberry — last element
System.out.println(fruits[fruits.length-1]);// Elderberry — safe last element

// ── Modifying elements: ───────────────────────────────────────────────
int[] numbers = {10, 20, 30, 40, 50};
numbers[2] = 99;    // replace 30 with 99
System.out.println(Arrays.toString(numbers));  // [10, 20, 99, 40, 50]

// ── ArrayIndexOutOfBoundsException: ──────────────────────────────────
int[] arr = {1, 2, 3};   // valid indices: 0, 1, 2

try {
    System.out.println(arr[3]);   // RUNTIME ERROR — index 3, length is 3
} catch (ArrayIndexOutOfBoundsException e) {
    System.err.println("Caught: " + e.getMessage());  // Index 3 out of bounds for length 3
}

try {
    System.out.println(arr[-1]);  // RUNTIME ERROR — negative index
} catch (ArrayIndexOutOfBoundsException e) {
    System.err.println("Caught: " + e.getMessage());  // Index -1 out of bounds for length 3
}

// ── Common off-by-one bugs: ───────────────────────────────────────────
int[] data = new int[5];

// BUG: i <= data.length accesses index 5 (out of bounds):
// for (int i = 0; i <= data.length; i++) { data[i] = i; }  // throws!

// CORRECT: i < data.length stops at index 4:
for (int i = 0; i < data.length; i++) {
    data[i] = i * 10;
}
System.out.println(Arrays.toString(data));  // [0, 10, 20, 30, 40]

// ── NullPointerException with uninitialised reference arrays: ─────────
String[] words = new String[3];   // [null, null, null]
try {
    int len = words[0].length();  // NullPointerException — words[0] is null
} catch (NullPointerException e) {
    System.err.println("words[0] is null — not yet initialised");
}
words[0] = "Hello";
System.out.println(words[0].length());  // 5 — now works

Arrays Utility Class

The java.util.Arrays class provides a comprehensive set of static utility methods for working with arrays. These methods cover the most common array operations: sorting, searching, filling, copying, comparing, and converting to string for printing. Using Arrays utilities rather than writing loops by hand produces cleaner, faster, and more readable code. Arrays.toString() is the most frequently useful method — it converts any array to a human-readable string like [1, 2, 3]. Without it, printing an array directly gives the default object representation (the class name and hashcode) which is useless for debugging. For multidimensional arrays, Arrays.deepToString() recursively formats nested arrays. Arrays.sort() sorts a primitive array or Object array in place using an optimised algorithm — dual-pivot quicksort for primitives, TimSort for objects. For a portion of an array, sort(array, fromIndex, toIndex) sorts only the specified range. Arrays.binarySearch() performs O(log n) search on a sorted array — it must only be called on arrays that are already sorted, otherwise the result is undefined. Arrays.copyOf() and Arrays.copyOfRange() create new arrays with copies of data — the original is unmodified. Arrays.fill() sets all (or a range of) elements to a specified value. Arrays.equals() compares two arrays element by element, and Arrays.deepEquals() compares multidimensional arrays recursively.
Java
import java.util.Arrays;

int[] numbers = {5, 3, 8, 1, 9, 2, 7, 4, 6};

// ── Arrays.toString() — readable output: ─────────────────────────────
System.out.println(numbers);               // [I@1b6d3586  — useless!
System.out.println(Arrays.toString(numbers)); // [5, 3, 8, 1, 9, 2, 7, 4, 6]

// ── Arrays.sort() — sorts in place: ──────────────────────────────────
Arrays.sort(numbers);
System.out.println(Arrays.toString(numbers)); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

// Sort a range [fromIndex, toIndex):
int[] partial = {5, 3, 8, 1, 9, 2, 7};
Arrays.sort(partial, 2, 5);   // sorts indices 2,3,4 only
System.out.println(Arrays.toString(partial)); // [5, 3, 1, 8, 9, 2, 7]

// ── Arrays.binarySearch() — O(log n) search on sorted array: ─────────
int[] sorted = {10, 20, 30, 40, 50, 60, 70};
int index = Arrays.binarySearch(sorted, 40);
System.out.println("Found 40 at index: " + index);  // 3
int missing = Arrays.binarySearch(sorted, 35);
System.out.println("35 not found: " + missing);     // negative (insertion point)

// ── Arrays.fill() — set all elements to a value: ─────────────────────
int[] zeros = new int[5];
Arrays.fill(zeros, 7);
System.out.println(Arrays.toString(zeros));  // [7, 7, 7, 7, 7]
Arrays.fill(zeros, 1, 4, 0);                // fill indices 1,2,3 with 0
System.out.println(Arrays.toString(zeros));  // [7, 0, 0, 0, 7]

// ── Arrays.copyOf() — copy with new length: ──────────────────────────
int[] original = {1, 2, 3, 4, 5};
int[] shorter  = Arrays.copyOf(original, 3);      // [1, 2, 3]
int[] longer   = Arrays.copyOf(original, 8);      // [1, 2, 3, 4, 5, 0, 0, 0]
int[] range    = Arrays.copyOfRange(original, 1, 4); // [2, 3, 4]
System.out.println(Arrays.toString(shorter));
System.out.println(Arrays.toString(longer));
System.out.println(Arrays.toString(range));

// ── Arrays.equals() — element-by-element comparison: ─────────────────
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
int[] c = {1, 2, 4};
System.out.println(Arrays.equals(a, b));    // true
System.out.println(Arrays.equals(a, c));    // false
System.out.println(a.equals(b));            // false — reference comparison!

Related Topics in Arrays

One-Dimensional Array
A one-dimensional array is a linear sequence of elements of the same type, accessed by a single integer index. It is the simplest array form in Java and the foundation for understanding multidimensional arrays and collection classes. One-dimensional arrays are used to store and process lists of values — student grades, product prices, daily temperatures, character sequences — any time you need to work with a fixed-size ordered collection of homogeneous data.
Two-Dimensional Array
A two-dimensional array in Java is an array of arrays — each element of the outer array is itself a one-dimensional array. It is used to represent tabular data with rows and columns: matrices, game boards, spreadsheet grids, pixel buffers, and any data that is naturally organised in two dimensions. In Java, a 2D array is declared with two sets of brackets and accessed with two indices: array[row][column].
Multidimensional Array
A multidimensional array in Java is an array with more than two dimensions — an array of arrays of arrays, and so on. While two-dimensional arrays suffice for most programming tasks, three-dimensional arrays are used for spatial data (x, y, z coordinates of a 3D grid), volumetric data (layers, rows, columns), RGB pixel buffers, and scientific computations involving tensors. Java supports arbitrary dimensionality, though arrays beyond three dimensions are rare in practice.
Jagged Array
A jagged array (also called a ragged array) is a multidimensional array where the inner arrays have different lengths. Unlike a rectangular 2D array where every row has the same number of columns, a jagged array allows each row to have its own independently sized inner array. This is possible because Java implements multidimensional arrays as arrays of arrays, making each inner array an independent object that can have any length.