Methods & arrays: interview questions & scenarios
These are the methods and arrays questions I reach for when I want to see whether a candidate understands what the JVM does on a call, or has only memorised slogans. Most of them fit on a whiteboard in five lines, which is exactly why they're good at separating the two.
Here's how to use this page: read the question, answer it out loud first, then open the model answer. The output questions are the ones people get wrong fastest, so commit to an answer before you peek.
Is Java pass-by-value or pass-by-reference?
Tests parameter passing, references
Show answerHide answer
Model answer
Always pass-by-value. On each call the JVM pushes a new frame and copies each argument into a fresh parameter slot. For a primitive the copy is the number. For an object, the variable's value is a reference, so the copy is a copy of the reference: both caller and callee point at the same heap object. The callee can mutate that object, but reassigning the parameter only re-points its own copy. That's why you can't write a swap(a, b) in Java that swaps the caller's two variables. See Methods and why Java is always pass-by-value.
How I’d say it in the interview
"Pass-by-value, always. For objects the value being copied is the reference, so I can change the object through it, but I can't make the caller's variable point at something else. The swap method that doesn't work is the proof."
Signals depth
Naming the stack frame and its local variable slots, and stating the mutate-versus-reassign distinction without being asked.
Red flags
"Primitives are passed by value and objects by reference." It's the most common wrong answer and interviewers wait for it.
Follow-ups interviewers ask next
What if I really need a method to "return" two updated values?
Return a small object, ideally a record:
record MinMax(int min, int max). A mutable holder such as anint[1]also works because you mutate the shared array, but it's harder to read and test.What if the language were truly pass-by-reference, like C++ with
int&?Then
swap(int& a, int& b)would swap the caller's variables, because the parameter would be an alias for the caller's storage. Java has no aliasing of variables; it only has copies of values, some of which happen to be references.
What does this print?
Tests mutation vs reassignment
Show answerHide answer
Model answer
static void change(StringBuilder sb) {
sb.append(" world");
sb = new StringBuilder("bye");
sb.append("!");
}
StringBuilder sb = new StringBuilder("hello");
change(sb);
System.out.println(sb);
It prints hello world. The first line mutates the shared object through the copied reference, so the caller sees it. The second line re-points only the callee's copy; "bye!" is built on a new object that becomes garbage on return.
How I’d say it in the interview
"hello world. The append goes through the shared reference, the reassignment doesn't leave the method."
Signals depth
Drawing two stack slots pointing at one heap object, then moving only the callee's arrow.
Red flags
Answering bye! (thinks it's pass-by-reference) or hello (thinks objects are copied).
Follow-ups interviewers ask next
What if the parameter were a String and the method did
s += " world";?The caller sees
hellounchanged.Stringis immutable, so+=builds a new string and reassigns the local copy. It's the same rule, with no mutator available.
What does this print?
Tests wrapper immutability, arrays as references
Show answerHide answer
Model answer
static void inc(Integer n) { n++; }
static void inc(int[] box) { box[0]++; }
Integer a = 5;
int[] b = {5};
inc(a);
inc(b);
System.out.println(a + " " + b[0]);
It prints 5 6. n++ on an Integer unboxes, adds one, boxes a new Integer and reassigns the local parameter: the caller's a still points at the old 5. The array is a shared heap object, so box[0]++ writes into the same array the caller holds.
How I’d say it in the interview
"5 then 6. The Integer increment is really a reassignment to a new box, while the array write mutates the shared array."
Signals depth
Expanding n++ into n = Integer.valueOf(n.intValue() + 1).
Red flags
Saying "6 6 because Integer is an object".
Which overload runs for each call?
Tests overload resolution phases
Show answerHide answer
Model answer
static void m(long x) { System.out.print("long "); }
static void m(Integer x) { System.out.print("Integer "); }
static void m(int... x) { System.out.print("varargs "); }
byte b = 1;
m(b); // long
m(5); // long
m(Integer.valueOf(5)); // Integer
m(); // varargs
It prints long long Integer varargs. Overload resolution runs in three phases (JLS §15.12.2): phase 1 allows only identity and widening, phase 2 adds boxing and unboxing, phase 3 adds varargs. byte and int both widen to long in phase 1, so m(Integer) is never considered. Integer.valueOf(5) is an identity match for m(Integer) in phase 1 (reaching long would need unboxing). m() only fits varargs. See Overloading and varargs.
How I’d say it in the interview
"Widening beats boxing, boxing beats varargs, because the compiler tries those in three separate phases and stops at the first that matches."
Signals depth
Saying the phases exist for backward compatibility with pre-Java 5 code.
Red flags
"Integer, because it's the closest type to int."
Follow-ups interviewers ask next
What if I call
m(null)?Compile error:
reference to m is ambiguous, betweenm(Integer)andm(int...). In phases 1 and 2 a varargs method still competes as if its last parameter were a plainint[], andnullfits bothIntegerandint[]. Neither type is a subtype of the other, so there's no most specific method.What if I delete
m(int...)and addm(Object x)?Now
m(null)printsInteger.nullcan't become along, andIntegeris more specific thanObject. Addm(String x)as well and it's ambiguous again, becauseStringandIntegerare unrelated. A cast such asm((String) null)settles it.
Can you overload a method by changing only its return type?
Tests method signature
Show answerHide answer
Model answer
No. The signature is the method name plus parameter types (JLS §8.4.2). Return type, parameter names and the throws clause don't count, so int f() and String f() in one class is "already defined". The reason: a call like f(); that ignores the result gives the compiler nothing to choose with.
How I’d say it in the interview
"No, the signature is name plus parameter types. A call can discard its result, so the return type can't be what picks the method."
Signals depth
Mentioning that the class file descriptor does include the return type (bridge methods rely on that), but the Java language forbids it.
Red flags
"Yes, if the return types are different classes."
Follow-ups interviewers ask next
What if the two methods are
f(List<String>)andf(List<Integer>)?Compile error: name clash, same erasure. Both erase to
f(List), so the JVM couldn't tell them apart.
A teammate wrote equals(Point other) on a class. Why does set.contains() return false?
Tests overloading vs overriding, compile-time binding
Show answerHide answer
Model answer
final class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
public boolean equals(Point o) { return o != null && x == o.x && y == o.y; }
@Override public int hashCode() { return 31 * x + y; }
}
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.contains(new Point(1, 2)); // false
equals(Point) overloads Object.equals(Object), it doesn't override it. HashSet calls equals(Object) through an Object-typed reference, so overload resolution picks the inherited identity check. The hash codes match, the bucket is right, and identity equality still says no. Fix: declare equals(Object o) and put @Override on it, which turns this mistake into a compile error.
How I’d say it in the interview
"It's an overload, not an override. Overloads are chosen at compile time by static type, and the collection only knows it has an Object, so it calls Object's equals."
Signals depth
Connecting it to the rule that overloading is compile-time, overriding is runtime, and recommending @Override as the guard.
Red flags
Blaming hashCode, or suggesting a TreeSet instead.
Follow-ups interviewers ask next
What if the caller writes
p.equals(new Point(1, 2))withPoint p?That returns
true: the static type isPoint, soequals(Point)is chosen. The same object compared through anObjectvariable returnsfalse, which is what makes this bug confusing.
What do these varargs calls return?
Tests varargs as arrays
Show answerHide answer
Model answer
static int count(Object... items) { return items == null ? -1 : items.length; }
count(); // 0
count((Object) null); // 1
count(1, "two", null); // 3
count(new Object[]{"a", "b"}); // 2
count((Object) new Object[]{"a"}); // 1
Varargs compiles to an array parameter, and the call site builds the array. No arguments means an empty array, never null. A single argument that is already an Object[] is passed as the array itself; casting it to Object forces it to be wrapped as one element. A bare count(null) compiles with a warning and passes a null array, returning -1.
How I’d say it in the interview
"Varargs is an array the compiler allocates. Zero args gives length zero; an existing array is passed straight through unless I cast it to Object."
Signals depth
Knowing the ACC_VARARGS flag and that each call allocates a new array (which is why APIs add fixed-arity overloads).
Red flags
"count() gets null."
What is heap pollution, and what does @SafeVarargs promise?
Tests generics, erasure, varargs
Show answerHide answer
Model answer
Heap pollution (JLS §4.12.2) is when a variable of a parameterized type refers to an object that isn't of that type. Generic varargs cause it because T... can't create a real T[] after erasure, so the compiler creates an Object[] and warns. If the method stores into that array or lets it escape, a caller can get a ClassCastException far from the cause:
static <T> T[] toArray(T... items) { return items; }
static <T> T[] pickTwo(T a, T b, T c) { return toArray(a, b); }
String[] s = pickTwo("x", "y", "z"); // ClassCastException: Object[] to String[]
@SafeVarargs is the author's promise that the method only reads the elements. It's allowed on constructors, static and final methods, and since Java 9 on private methods: places that can't be overridden to break the promise.
How I’d say it in the interview
"Generic varargs makes an Object array behind a T-array type. If the method leaks or writes that array, the types lie. SafeVarargs says I only read from it, and it's limited to methods nobody can override."
Signals depth
The pickTwo example, and knowing why the annotation is restricted to non-overridable methods.
Red flags
Treating @SafeVarargs as a way to make warnings go away without checking the method body.
Follow-ups interviewers ask next
What if the method passes the array to
Arrays.asList(items)and returns the list?javac -Xlintwarns that the varargs array escapes. The list is backed by the array, so the method is no longer purely reading. Copy the elements into a newArrayListinstead.
Does this code compile, and what happens at runtime?
Tests array covariance
Show answerHide answer
Model answer
Object[] objs = new String[2];
objs[0] = "ok";
objs[1] = 42;
It compiles, because arrays are covariant: a String[] is an Object[]. At runtime the aastore instruction checks every reference store against the array's actual element type, so objs[1] = 42 throws ArrayStoreException: java.lang.Integer.
How I’d say it in the interview
"Compiles, then throws ArrayStoreException on the Integer store. Arrays remember their element type and check stores at runtime."
Signals depth
Contrasting it with generics, where List<Object> l = new ArrayList<String>() is a compile error, and explaining that this is why generic array creation is banned.
Red flags
"It's a compile error" or "it stores 42 fine".
Follow-ups interviewers ask next
What if it were
List<Object> list = new ArrayList<String>();?Compile error: generics are invariant. The check moves from runtime to compile time, which is the main reason generics are preferred over arrays for APIs.
This code returns false and then throws. Why?
Tests Arrays.asList
Show answerHide answer
Model answer
int[] ids = {1, 2, 3};
Arrays.asList(ids).contains(2); // false
List<String> tags = Arrays.asList("a", "b");
tags.add("c"); // UnsupportedOperationException
asList(T... a) can't take primitives as T, so with an int[] the whole array becomes the single element: you get a List<int[]> of size 1. Second, asList returns a fixed-size view backed by the array: set writes through, but add and remove throw. Fixes: IntStream.of(ids).boxed().toList() for primitives, and new ArrayList<>(Arrays.asList(...)) when you need to grow.
How I’d say it in the interview
"asList is a fixed-size view over the array, and generics can't hold primitives, so an int array turns into a one-element list."
Signals depth
Mentioning that writes through set change the original array, and that List.of is immutable and rejects nulls.
Red flags
Suggesting a cast to List<Integer>.
Production throws StackOverflowError when parsing deeply nested input. What do you do?
Tests call stack, -Xss, recursion
Show answerHide answer
Model answer
Each recursive call pushes a frame, and a thread's stack is fixed (1 MB by default on 64-bit Linux, set with -Xss). Nested input like a 50,000-level JSON array drives a recursive parser past it. Options, in my order of preference:
- Cap nesting depth and reject input past it (say 1,000 levels). This also blocks a denial-of-service vector.
- Rewrite as iteration with an explicit
ArrayDequeas the stack, so depth is limited by heap, not thread stack. - Run the work on a dedicated thread with a bigger stack:
new Thread(null, task, "parser", 64L << 20). - Raise
-Xssglobally only as a last resort: it applies to every thread.
Catching StackOverflowError and continuing is unsafe, because it can fire mid-update inside any method.
How I’d say it in the interview
"It's unbounded recursion against a fixed-size stack. I'd cap the depth first, then make the algorithm iterative. Raising Xss for the whole JVM is a blunt tool."
Signals depth
Seeing the security angle (attacker-controlled depth), and knowing HotSpot doesn't eliminate tail calls.
Red flags
"Increase the heap", or "wrap it in try/catch".
Follow-ups interviewers ask next
What if it only fails on some servers?
Frame sizes differ between interpreted and JIT-compiled code, and default stack sizes differ by OS and CPU (2 MB on Apple Silicon macOS, 1 MB on Linux x64). The overflow depth isn't a fixed number, so treat any value near the limit as a bug.
Write a method that deep-copies a jagged int[][].
Tests arrays of arrays, clone semantics
Show answerHide answer
Model answer
static int[][] deepCopy(int[][] src) {
if (src == null) return null;
int[][] out = new int[src.length][];
for (int i = 0; i < src.length; i++) {
out[i] = src[i] == null ? null : src[i].clone();
}
return out;
}
src.clone() alone is shallow: the new outer array holds the same row references, so a write to copy[0][0] shows up in the original. Allocating the outer array with new int[n][] and cloning each row copies everything, preserves each row's length and handles null rows. For an Object[][] you'd still share the element objects; that's as deep as an array copy goes.
How I’d say it in the interview
"A 2D array is an array of rows, so clone only copies the row references. I allocate a new outer array and clone each row, keeping jagged lengths and null rows."
Signals depth
Saying int[] clone() is fine for a row because primitives are copied by value, and mentioning Arrays.copyOf(row, row.length) as an equivalent.
Red flags
Using new int[src.length][src[0].length], which breaks on jagged input.
Follow-ups interviewers ask next
What if you then sum the copy, which loop order do you use?
Row index outside, column inside. Each row is contiguous, so that walks memory sequentially through 64-byte cache lines; column-first jumps to a different row object every step and is around 10x slower on a 4096 x 4096 grid.