Skip to content

Arrays Interview Questions & Answers

24 questions Updated 2026-06-20 Share:

Java arrays interview questions — declaration and initialization, default values, multidimensional and jagged arrays, array covariance, Arrays utility methods, copying, and array vs ArrayList.

Read the in-depth guideJava Arrays — Declaration, the Arrays Utility Class & Gotchas(opens in new tab)
24 of 24

An array is declared with type[] and created with new, an array initializer { ... }, or both. The brackets can sit on the type or the variable, but type[] is the convention.

int[] a = new int[3];          // size only — defaults {0,0,0}
int[] b = new int[]{1, 2, 3};  // size + values
int[] c = {1, 2, 3};           // shorthand initializer (declaration only)
int d[] = {1, 2, 3};           // legal but discouraged (C-style)

The bare { ... } shorthand works only at declaration — you can't write c = {4,5,6}; later; you'd need c = new int[]{4,5,6};. Either form fixes the length at creation.

An array is always an object on the heap, even an array of primitives. The variable holds a reference to it, the array has a runtime class (int[].class), and it inherits from Object — so you can call clone(), read .length, and store it in an Object variable.

int[] a = {1, 2, 3};
Object o = a;                  // arrays are Objects
System.out.println(a.getClass().getName()); // "[I" — int array
int[] copy = a.clone();        // inherited (overridden) clone

Because it's an object, an array variable can be null, and elements of a primitive array are stored inline (contiguously) while elements of an object array are references.

Allocating an array with new zero-initializes every element — you never get garbage. The default depends on the element type:

Element type Default
numeric (int, double, …) 0 / 0.0
boolean false
char '' (null char)
reference (String, objects) null
int[] nums = new int[3];        // {0, 0, 0}
boolean[] flags = new boolean[2]; // {false, false}
String[] names = new String[2]; // {null, null}

This differs from local variables, which get no default and must be assigned before use. Array slots, like fields, are always initialized.

No. An array's length is fixed at creation and can never change. To "grow" one you allocate a new, larger array and copy the elements over (which is exactly what ArrayList does internally).

int[] a = {1, 2, 3};
// a = a + one element  -> not possible
int[] bigger = Arrays.copyOf(a, a.length + 1); // new array, last slot = 0
bigger[3] = 4;          // {1, 2, 3, 4}

If you need a resizable, append-friendly structure, reach for ArrayList instead of hand-rolling array growth.

Three different things that beginners confuse:

  • array.length — a field (no parentheses) giving an array's size.
  • string.length() — a method giving a String's character count.
  • collection.size() — a method on List/Set/Map giving its element count.
int[] a = {1, 2, 3};
String s = "hello";
List<Integer> list = List.of(1, 2);

a.length     // 3  — field, no ()
s.length()   // 5  — method
list.size()  // 2  — method

Writing a.length() or s.length is a compile error — memorize which is which.

Java performs bounds checking on every access. A valid index is 0 to length - 1; anything else throws ArrayIndexOutOfBoundsException at runtime (it's unchecked, so it isn't caught at compile time).

int[] a = {10, 20, 30};
a[0];    // 10 — first element
a[2];    // 30 — last (length - 1)
a[3];    // ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
a[-1];   // also AIOOBE — no negative indexing in Java

Unlike some languages, Java has no negative indexing; a[-1] is an error, not "last element". Bounds checks are what make Java memory-safe here.

The size is evaluated at runtime, and a negative length throws NegativeArraySizeException. A size of 0 is perfectly legal — it gives a valid, empty array.

int[] ok    = new int[0];   // legal — empty array, length 0
int n = -1;
int[] bad   = new int[n];   // NegativeArraySizeException at runtime

This catches interviewees who assume any compile-passing new int[expr] is safe — a computed negative size blows up only when the line executes.

Java has no true 2D arrays — int[][] is an array of arrays. The outer array holds references to inner arrays, each a separate heap object.

int[][] grid = new int[2][3];  // 2 rows, each an int[3]
grid[0][1] = 5;
grid.length      // 2 — number of rows
grid[0].length   // 3 — columns in row 0

for (int[] row : grid)         // each row is itself an int[]
  for (int v : row) { /* ... */ }

Because the inner arrays are independent objects, accessing grid[i][j] is two pointer hops. You can also declare more dimensions (int[][][]) the same way.

A jagged array is a multidimensional array whose inner rows have different lengths. Since each row is a separate array object, this is fully legal — you just allocate the rows yourself.

int[][] jagged = new int[3][];   // 3 rows, none allocated yet
jagged[0] = new int[]{1};        // length 1
jagged[1] = new int[]{2, 3};     // length 2
jagged[2] = new int[]{4, 5, 6};  // length 3

for (int[] row : jagged)
  System.out.println(row.length); // 1, 2, 3

Always iterate with row.length rather than a fixed column count, since rows can differ — and an unallocated row stays null until you assign it.

A primitive array stores the values inline; an object array stores references, and creating it does not create the objects — every slot starts as null.

int[] nums = new int[2];        // {0, 0} — values ready to use
String[] names = new String[2]; // {null, null} — no String objects yet

names[0].length();              // NullPointerException — slot is null!
names[0] = "Ada";               // must populate first
names[0].length();              // now 3

The classic bug: iterating an object array and calling methods before filling it, which throws NullPointerException. Primitive arrays never have this problem.

Java arrays are covariant: if Sub extends Super, then Sub[] is a subtype of Super[]. So you can assign a String[] to an Object[] variable. This is convenient but not type-safe at compile time.

Object[] arr = new String[3];   // legal — covariance
arr[0] = "ok";                  // fine, it's really a String[]
arr[1] = 42;                    // compiles, but throws at runtime

Contrast with generics, which are invariantList<String> is not a List<Object>. Generics chose compile-time safety; arrays chose flexibility, pushing the check to runtime (see ArrayStoreException).

Because arrays are covariant, the JVM must check at runtime that every value you store is compatible with the array's actual element type. Storing an incompatible type throws ArrayStoreException.

Object[] arr = new String[2];   // actual type is String[]
arr[0] = "hi";                  // OK
arr[0] = Integer.valueOf(42);   // ArrayStoreException at runtime

This runtime store check is the price of covariance, and it's why generics were designed to be invariant — the compiler can guarantee type safety for List<String>, so no equivalent check is needed.

Arrays.sort sorts in place. Primitive arrays use a tuned dual-pivot quicksort; object arrays use a stable merge sort and can take a Comparator.

int[] nums = {3, 1, 2};
Arrays.sort(nums);                  // {1, 2, 3} — natural order

String[] words = {"banana", "apple"};
Arrays.sort(words);                 // natural (lexicographic)
Arrays.sort(words, Comparator.reverseOrder()); // {"banana", "apple"}
Arrays.sort(words, Comparator.comparingInt(String::length)); // by length

Arrays.sort(nums, 0, 2);            // sort only a sub-range [0,2)

Note primitive arrays can't take a Comparator (no objects to compare) — box to Integer[] if you need custom ordering of numbers.

Arrays.parallelSort (Java 8+) sorts large arrays using a parallel merge-sort across the common ForkJoinPool, then merges the pieces. Same result as Arrays.sort, faster on big arrays and multiple cores.

int[] big = new int[10_000_000];
// ... fill big ...
Arrays.parallelSort(big);    // multi-threaded sort

It only pays off above a size threshold (roughly a few thousand elements); below that it falls back to a sequential sort, so for small arrays plain Arrays.sort is simpler and just as fast.

Three everyday Arrays helpers:

  • fill — sets every element (or a range) to one value.
  • equals — element-by-element value comparison (== on arrays only checks references).
  • toString — a readable "[1, 2, 3]" (an array's own toString prints [I@1b6d3586).
int[] a = new int[3];
Arrays.fill(a, 7);              // {7, 7, 7}

int[] b = {7, 7, 7};
a == b              // false — different objects
Arrays.equals(a, b) // true  — same contents

System.out.println(a);                 // [I@... (unhelpful)
System.out.println(Arrays.toString(a)); // [7, 7, 7]

Always use Arrays.equals/Arrays.toString — the inherited Object versions compare references and print the type+hash.

The plain Arrays.equals and Arrays.toString are shallow — for a 2D array they compare/print the inner arrays by reference, which is wrong. The deep variants recurse into nested arrays.

int[][] a = {{1, 2}, {3, 4}};
int[][] b = {{1, 2}, {3, 4}};

Arrays.equals(a, b)      // false — compares inner int[] references
Arrays.deepEquals(a, b)  // true  — recurses into rows

Arrays.toString(a)       // [[I@..., [I@...]
Arrays.deepToString(a)   // [[1, 2], [3, 4]]

Rule: use deepEquals/deepToString for multidimensional or nested arrays, the shallow ones for flat arrays.

Both return a new array. copyOf copies from the start to a given length (truncating or zero-padding if longer); copyOfRange copies a [from, to) slice.

int[] a = {1, 2, 3};
Arrays.copyOf(a, 2);          // {1, 2}        — truncated
Arrays.copyOf(a, 5);          // {1, 2, 3, 0, 0} — padded with defaults
Arrays.copyOfRange(a, 1, 3);  // {2, 3}        — indices 1..2 (to is exclusive)

copyOf(a, a.length + n) is the standard idiom for "grow this array". Both are shallow copies for object arrays (they copy references, not the objects).

System.arraycopy is a native, low-level bulk copy into an existing destination array: arraycopy(src, srcPos, dest, destPos, length). It's the fastest copy and what copyOf/ArrayList use under the hood — but you must allocate the destination yourself.

int[] src = {1, 2, 3, 4, 5};
int[] dest = new int[5];
System.arraycopy(src, 1, dest, 0, 3); // dest = {2, 3, 4, 0, 0}

Use System.arraycopy when copying into an array you already have (or inserting a range); use Arrays.copyOf when you just want a fresh copy and let it do the allocation.

clone() makes a shallow copy: a new array of the same length with the same elements. For a primitive array that's effectively a deep copy. For an object (or 2D) array, the references are copied — both arrays point at the same objects.

int[] a = {1, 2, 3};
int[] b = a.clone();
b[0] = 99;            // a unchanged — independent (primitives)

int[][] grid = {{1, 2}, {3, 4}};
int[][] copy = grid.clone();
copy[0][0] = 99;      // grid[0][0] is ALSO 99 — shared inner array!

To truly deep-copy a 2D array, clone each row yourself: for (int i = 0; i < grid.length; i++) copy[i] = grid[i].clone();.

Arrays.asList returns a fixed-size List backed by the array — it's a view, not a normal ArrayList. You can read and set, but add/remove throw UnsupportedOperationException, and changes write through to the array.

Integer[] arr = {1, 2, 3};
List<Integer> list = Arrays.asList(arr);
list.set(0, 99);     // OK — also changes arr[0] to 99
list.add(4);         // UnsupportedOperationException — fixed size

// also: passing an int[] gives a List<int[]> of size 1, not List<Integer>!
List<Integer> wrong = Arrays.asList(1, 2, 3); // OK with varargs of Integer

// for a real mutable list:
List<Integer> real = new ArrayList<>(Arrays.asList(arr));

Two traps: it's not resizable, and Arrays.asList(intArray) boxes the array itself (a one-element List<int[]>) — use an Integer[] or a stream.

The standard bridges:

int[] prims = {1, 2, 3};
IntStream is = Arrays.stream(prims);     // primitive array -> IntStream
int[] back  = is.map(x -> x * 2).toArray();

String[] arr = {"a", "b"};
List<String> list = new ArrayList<>(Arrays.asList(arr)); // array -> List
String[] arr2 = list.toArray(new String[0]);            // List -> array

Stream<String> s = Arrays.stream(arr);   // object array -> Stream
String[] arr3 = s.toArray(String[]::new);

Note Arrays.stream(int[]) gives an IntStream (no boxing), whereas Stream.of(int[]) would give a Stream<int[]> of one element — prefer Arrays.stream for primitive arrays.

A varargs parameter (Type... args) is an array — the compiler packages the passed arguments into a Type[]. Inside the method args is a normal array with .length and indexing.

static int sum(int... nums) {     // nums is really an int[]
  int total = 0;
  for (int n : nums) total += n;  // iterate like any array
  return total;
}
sum(1, 2, 3);          // compiler builds new int[]{1, 2, 3}
sum(new int[]{1, 2});  // can also pass an array directly
sum();                 // empty array, length 0 (not null)

Because it's just an array, varargs must be the last parameter, and passing an existing array works directly. An empty call yields a zero-length array, not null.

Aspect Array ArrayList
Size fixed at creation dynamic (grows/shrinks)
Element type primitives or objects objects only (boxes primitives)
Access a[i], field .length get(i)/set, method .size()
Type safety covariant (runtime check) generic, invariant (compile-time)
Performance no boxing, less overhead boxing + resize cost
int[] arr = new int[3];          // fixed, primitive, fast
List<Integer> list = new ArrayList<>(); // resizable, boxes ints
list.add(1); list.add(2);        // grows automatically

Choose an array for fixed-size, primitive-heavy, performance-critical code; choose ArrayList when the size varies or you want the rich List API. ArrayList is itself backed by a resizing array.

More ways to practice

The self-quiz is live. Get notified when mock interviews and new question packs drop.

or
Join our WhatsApp Channel