☕ Java
Returning Arrays
A Java method can return an array by declaring the return type with brackets and using a return statement with an array value. The returned value is a reference to the array object — the caller receives this reference and can use it to access the array's elements. Returning arrays from methods is the standard mechanism for methods that compute collections of results, generate sequences, transform input data into output data, and filter arrays to produce subsets.
Returning Arrays — Syntax and Semantics
The syntax for returning an array is straightforward — declare the return type with brackets (int[], String[], double[][]) and use a return statement with an array reference. The returned value is always a reference to the array on the heap, never a copy of the array's data. The caller receives this reference and can use it to access and modify the returned array.
Because the return value is a reference, the array continues to exist on the heap as long as the caller holds the reference. If the method returns a reference to a locally created array, that array outlives the method invocation — it is not destroyed when the method returns. The JVM's garbage collector keeps an object alive as long as any reference points to it.
The caller should always handle a returned array with the awareness that it is a reference to a specific object. If the returned array should be independent from any other array, the method must return a newly created array, not a reference to an array held internally by the method's class (unless the intention is to expose the internal array, which has implications for encapsulation).
A returned array can be used directly in expressions without assigning it to a variable. Calling method()[index] accesses an element of the returned array inline. This works because the array reference is evaluated first, and then the index operator is applied to it. However, if the array will be used more than once, assigning it to a variable avoids calling the method multiple times unnecessarily.
Java
// ── Method returning an array: ───────────────────────────────────────
public static int[] createRange(int start, int end) {
if (start > end) return new int[0]; // empty array, not null
int[] result = new int[end - start + 1];
for (int i = 0; i < result.length; i++) {
result[i] = start + i;
}
return result; // return reference to the new array
}
int[] range = createRange(3, 8);
System.out.println(Arrays.toString(range)); // [3, 4, 5, 6, 7, 8]
// ── Use returned array inline (no variable needed): ───────────────────
System.out.println(createRange(1, 5)[2]); // 3 — third element of [1,2,3,4,5]
System.out.println(Arrays.toString(createRange(10, 15))); // [10,11,12,13,14,15]
// ── Returning a 2D array: ─────────────────────────────────────────────
public static int[][] createIdentityMatrix(int n) {
int[][] matrix = new int[n][n];
for (int i = 0; i < n; i++) {
matrix[i][i] = 1; // diagonal elements = 1, rest = 0 by default
}
return matrix;
}
int[][] identity = createIdentityMatrix(4);
for (int[] row : identity) {
System.out.println(Arrays.toString(row));
}
// [1, 0, 0, 0]
// [0, 1, 0, 0]
// [0, 0, 1, 0]
// [0, 0, 0, 1]
// ── Array initialiser shorthand in return statement: ──────────────────
public static String[] getDayNames() {
return new String[]{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
// return {"Mon", "Tue", ...}; // COMPILE ERROR — initialiser needs 'new'
// When used as a return value (not in a declaration), must use 'new'.
}
String[] days = getDayNames();
System.out.println(Arrays.toString(days));Returning Empty Arrays Instead of Null
A method that computes results and returns them as an array faces a design decision when there are no results: return null or return an empty array. Returning null is the wrong choice in almost every situation. Returning null forces every caller to perform a null check before using the result — and if any caller forgets, a NullPointerException is thrown. This null-check burden scales with the number of callers and makes the code more verbose and error-prone everywhere the method is used.
Returning an empty array (new int[0] or new int[]{}) is always safe. The caller can iterate over it with a for loop — the loop body simply executes zero times. The caller can check its length — it returns 0. The caller can pass it to other methods — they receive a valid, non-null array. No special case handling is required anywhere. This is the principle of returning "empty collections rather than null" from Effective Java.
Empty arrays are not expensive — they are small objects with a length of zero. Creating one is a single allocation of a fixed small size. For frequently called methods that often return empty arrays, a shared constant empty array can be used: a single static final empty array that all callers receive a reference to. Since they cannot modify a zero-length array (there are no elements to modify), sharing is safe.
Java
// ── WRONG — returning null forces callers to null-check everywhere: ────
public static int[] findEvens(int[] data) {
if (data == null || data.length == 0) return null; // BAD
List<Integer> evens = new ArrayList<>();
for (int n : data) {
if (n % 2 == 0) evens.add(n);
}
return evens.isEmpty() ? null : evens.stream().mapToInt(i->i).toArray();
}
int[] result = findEvens(new int[]{1, 3, 5});
// MUST null-check before use:
if (result != null) { // extra burden on every caller
System.out.println(result.length);
}
// Missing this null check = NullPointerException at runtime.
// ── RIGHT — always return an empty array, never null: ─────────────────
// Shared empty array constant — avoid repeated allocation:
private static final int[] EMPTY_INT_ARRAY = new int[0];
public static int[] findEvens(int[] data) {
if (data == null || data.length == 0) return EMPTY_INT_ARRAY; // safe
List<Integer> evens = new ArrayList<>();
for (int n : data) {
if (n % 2 == 0) evens.add(n);
}
return evens.stream().mapToInt(i -> i).toArray();
// If evens is empty, mapToInt returns new int[0] — safe
}
// ── Callers never need null checks: ───────────────────────────────────
int[] result = findEvens(new int[]{1, 3, 5});
System.out.println(result.length); // 0 — no NPE
for (int n : result) { // loop body executes 0 times
System.out.println(n); // never reached
}
int[] result2 = findEvens(new int[]{2, 4, 6, 8});
System.out.println(Arrays.toString(result2)); // [2, 4, 6, 8]
// Same code — no special casing needed.Defensive Copies When Returning Internal Arrays
When a class has an internal array field and a method returns that array, the caller receives a direct reference to the internal array. This breaks encapsulation — the caller can now modify the class's internal state without going through any of the class's methods, bypassing any validation or invariant enforcement. This is called an internal array leak or representation exposure.
The solution is to return a defensive copy — a new array with the same element values — rather than returning the internal reference. The caller receives a copy and can do whatever they want with it without affecting the class's internal state. The class's invariants are preserved.
Defensive copies in return values are the mirror of defensive copies in constructors and setters. Together they form the complete protection for a class's internal state: make a defensive copy of mutable arguments when storing them (constructor), and make a defensive copy when returning mutable internal state (getter). This is the pattern used by all well-designed immutable or encapsulated classes in the Java standard library.
The performance cost of defensive copying is real — every call to a getter that returns a copy allocates a new array and copies all elements, which is O(n). For small arrays or infrequent access this is negligible. For large arrays or very frequent access, returning an unmodifiable view, returning a stream, or using an accessor pattern may be more appropriate than copying the entire array on every call.
Java
// ── VULNERABLE — returns direct reference to internal array: ─────────
public class GradeBook {
private final int[] grades; // internal state
public GradeBook(int[] grades) {
this.grades = grades.clone(); // defensive copy on construction
}
public int[] getGrades() {
return grades; // DANGEROUS — exposes internal array
}
}
GradeBook book = new GradeBook(new int[]{90, 85, 92});
int[] exposed = book.getGrades(); // direct reference to internal array
exposed[0] = 0; // corrupts internal state!
System.out.println(book.getGrades()[0]); // 0 — encapsulation broken
// ── SAFE — returns a defensive copy: ─────────────────────────────────
public class GradeBook {
private final int[] grades;
private double cachedAverage = -1; // invalidated when grades change
public GradeBook(int[] grades) {
if (grades == null || grades.length == 0)
throw new IllegalArgumentException("Grades cannot be empty");
this.grades = grades.clone(); // defensive copy in constructor
}
public int[] getGrades() {
return grades.clone(); // defensive copy on return
}
public double getAverage() {
if (cachedAverage < 0) {
int sum = 0;
for (int g : grades) sum += g;
cachedAverage = (double) sum / grades.length;
}
return cachedAverage;
}
public int getHighest() {
return Arrays.stream(grades).max().getAsInt();
}
}
GradeBook safe = new GradeBook(new int[]{90, 85, 92});
int[] copy = safe.getGrades(); // receives a copy
copy[0] = 0; // modifies only the copy
System.out.println(safe.getGrades()[0]); // 90 — internal state protected
// ── When defensive copy is too expensive — alternatives: ──────────────
public class LargeDataSet {
private final double[] data; // large — copying is expensive
public LargeDataSet(double[] data) {
this.data = data.clone();
}
// Option 1: Return a stream — no copy needed:
public java.util.stream.DoubleStream stream() {
return Arrays.stream(data); // stream over internal array safely
}
// Option 2: Provide indexed access — no copy needed:
public double get(int index) { return data[index]; }
public int size() { return data.length; }
// Option 3: Return Collections.unmodifiableList wrapper:
// (requires boxing doubles to Double — not ideal for primitives)
}Related Topics in Arrays
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.
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.