What Happens in Memory
Stack frames, heap objects and references: the picture behind every "why did my array change?" and every overflow bug.
01 · why it exists
Why this exists, and the weight it carries
since1960
Call stack & heap
Edsger W. Dijkstra
Early programs gave each subroutine one fixed block of working memory. A subroutine could not be called again before it finished, because the new call would overwrite the old one's data. In "Recursive Programming" (1960), written for ALGOL 60 compilers, Dijkstra kept each call's data on a stack instead: push a fresh area on every call, drop it on return. The JVM works the same way today: every thread has a stack of frames, and objects live on a shared heap.
the problem it solvedLet methods call each other, and themselves, without overwriting each other's local variables, and free that memory the instant a call returns.
its jobs today
- Every Java method call: the JVM pushes a frame with the parameters and locals, and pops it on return.
- Every
new: objects and arrays are allocated on the heap shared by all threads; frames hold only references to them. - Every exception's stack trace: it is a printout of the frames that were on the stack when things went wrong.
the weight it carries
Illustrative: the default 1 MB thread stack (64-bit Linux and macOS) ÷ about 100 bytes per small frame ≈ 10,000 nested calls. In our test (64-bit HotSpot, JDK 25 and 26 on Windows x64, -Xss1m), this chapter's recursive sumTo overflowed after about 8,900 calls interpreted (-Xint) and 11,000–41,000 with the JIT on, depending on how much of it had been compiled. Objects sit on the heap instead, which is sized separately (-Xmx) and is normally far larger.
without it
Without a frame per call, no method could call itself, so no tree DFS and no backtracking. Without a heap, nothing could outlive the method that made it: no linked lists, no HashMaps.
when · why · how
- when
- Whenever you ask: where does this variable live, and will it still exist after the method returns?
- why
- Stack memory is free to allocate and release (push and pop, O(1)) but small and short-lived. Heap memory is large and long-lived but cleaned by the GC. Recursion depth d costs O(d) stack.
- how
- Locals and parameters go in the frame;
newputs the object on the heap and gives you a reference. For depth beyond about 10⁴, switch to a loop or anArrayDeque, whose contents live on the heap.
since1996
References & pass-by-value
James Gosling, Bill Joy and Guy Steele (The Java Language Specification, first edition)
C and C++ let programs hold raw memory addresses and do arithmetic on them. Java's specification kept the idea of pointing at objects but limited what you can do with the pointer: a variable of class or array type holds a reference, and the JLS lists the few operations allowed on it, such as field access, method calls, casts and ==. Arithmetic is not one of them. The JLS also fixes how calls work: each argument's value initialises a brand-new parameter variable. So Java is pass-by-value, always; for objects, the value copied is the reference.
the problem it solvedShare big objects between methods cheaply, by copying one reference instead of the whole object, without letting code touch raw memory addresses.
its jobs today
- Passing an
int[]or aListto a helper costs O(1): only the reference is copied, never the n elements. - In-place algorithms (sort, reverse, move zeroes) change the caller's array through the copied reference.
- Linked lists, trees and graphs are nothing but objects holding references to other objects.
the weight it carries
Illustrative: a 10⁶-element int[] holds 4,000,000 bytes of data. Passing it to a helper copies one reference, a few bytes. If every call copied the array instead, a recursive helper 20 levels deep would copy 20 × 4 MB = 80 MB.
without it
Every method call would copy whole arrays and objects, O(n) per call, and in-place algorithms would be impossible.
when · why · how
- when
- A method must change its caller's data, or you are unsure whether a change will be visible outside the method.
- why
- Passing is O(1) because only references are copied. Changing the object through the reference is visible to the caller; reassigning the parameter is not.
- how
- Change contents (
arr[i] = …,list.add(…),node.next = …) to affect the caller. To give back several values, return anint[]or a record. Snapshot shared lists withnew ArrayList<>(path).
since1996 (fixed sizes) · 2006 (the binary search bug)
Fixed-width integers & overflow
Java's designers; Joshua Bloch
Java fixed the size of every primitive on every machine: int is always 32-bit two's complement, long 64-bit, and the JLS says the integer operators never signal overflow. That keeps arithmetic fast and portable, but silent. In 2006 Joshua Bloch showed that int mid = (low + high) / 2; in the JDK's own binary search, which he had written, breaks once arrays reach about 2³⁰ elements. He reported that the bug sat unnoticed in the JDK for about nine years, and that the same bug is in the binary search Jon Bentley proved correct in Programming Pearls.
the problem it solvedThe same answer on every platform using plain hardware arithmetic, at the price of a silent wrap-around you must plan for.
its jobs today
- Every array index, array
lengthand collectionsize()in Java is anint, which is why an array cannot hold more than about 2³¹ − 1 elements. - Hash codes are ints that are meant to wrap:
String.hashCodemultiplies by 31 and lets the overflow mix the bits. longcarries timestamps (System.currentTimeMillis()), file sizes and money in paise, where anintwould wrap.
the weight it carries
Illustrative: n = 10⁵ values of up to 10⁹ sum to 10¹⁴. That is about 46,600 times Integer.MAX_VALUE (2,147,483,647 ≈ 2.1·10⁹) but only 1/92,000 of Long.MAX_VALUE (≈ 9.2·10¹⁸). A midpoint lo + hi overflows once both are above 2³⁰ = 1,073,741,824.
without it
Without fixed sizes the same program could print different answers on different machines. Without knowing the limits you ship code that passes the small examples and fails the big hidden tests.
when · why · how
- when
- Values can pass about 2·10⁹: sums of many large values, products of two values above 46,340, prefix sums, "mod 10⁹ + 7" arithmetic, midpoints near 2³¹.
- why
- Overflow costs no time and raises no error, so it gives a wrong answer instead of a crash. A
longcosts the same O(1) per operation and 8 bytes instead of 4. - how
- Widen before the operation:
long sum,(long) a * b. For midpoints uselo + (hi - lo) / 2or(lo + hi) >>> 1. While debugging,Math.addExactandMath.multiplyExactthrow on overflow.
since1960
Garbage collection
John McCarthy
McCarthy's Lisp built every program out of small linked cells, and cells that nothing pointed to any more piled up. His 1960 paper describes a reclamation cycle that starts when free storage runs out: find every cell still reachable from the program, and return the rest to the free list. Java has never had a free or delete: the JVM specification says heap objects are never explicitly deallocated, only reclaimed by a garbage collector. Since JDK 9, G1 has been HotSpot's default collector on server configurations (JEP 248).
the problem it solvedFree memory automatically and safely, so a program can neither forget to free an object nor use one after freeing it.
its jobs today
- Reclaims every object your algorithm drops: popped stack nodes, unlinked list nodes, the old array after an
ArrayListgrows. - Lets data structures share sub-objects freely (two lists pointing at one node) without anyone tracking who must free it.
the weight it carries
Illustrative: OpenJDK's ArrayList grows its array by 1.5× (capacity 10 → 15 → 22 → …). After the first 10-slot array, reaching 10⁶ elements takes 29 more grow steps (up to 1,215,487 slots), so 29 old arrays become garbage along the way, and the GC frees them without a single line of your code.
without it
You would free every node by hand, as in C: forget one and memory leaks; free one too early and the program reads garbage.
when · why · how
- when
- Memory matters (10⁶+ objects, or a "Memory Limit Exceeded" verdict), or you write your own array-backed container.
- why
- The GC is automatic but frees only unreachable objects. Anything still referenced, even by a stale array slot, stays alive: memory you thought was freed.
- how
- Drop references you no longer need:
items[size] = nullafter a pop, clear big maps when done, preferint[]over millions of boxed objects. Never callSystem.gc()in a solution.
02 · plain english
The idea in 30 seconds
Every method call gets a frame on the call stack for its parameters and local variables. The frame disappears when the method returns.
Every object you create with new, every array included, lives on the heap. A variable of object type holds only a reference: an arrow to it.
Java always copies the value into a parameter. For an object, that value is the arrow, so a method can change the object but never the caller's variable.
picture this
Desktop shortcuts in Windows: the shortcut (a reference in your frame) is tiny and points at a real folder (the object on the heap). Copy the shortcut and both copies open the same folder, so a file added through one shows up in the other. Deleting a shortcut never deletes the folder.
03 · remember
What you must know
- Stack: one frame per running call, holding parameters, locals and partial results. Push on call, pop on return. Small: 1 MB per thread by default on 64-bit Linux and macOS.
- Heap: every object and array (
new). Shared, much bigger, and cleaned by the garbage collector once nothing can reach an object. - Primitive vs reference: an
intvariable holds the number itself. Anint[],StringorListNodevariable holds a reference (ornull). - Always pass-by-value.
swap(int a, int b)swaps copies.arr[0] = 99inside a method changes the caller's array, because both arrows point at one array.arr = new int[5]inside the method changes nothing outside. - `==` compares what the variables hold: values for primitives, references for objects. Compare contents with
equals(Arrays.equalsfor arrays). - `int` is 32 bits: about ±2.1·10⁹, and it wraps around silently. Widen to
longbefore the operation, and write midpoints aslo + (hi - lo) / 2. - Arrays are objects:
b = acopies the reference;a.clone()copies the contents; anint[][]is an array of references to separate rows. - Recursion depth = stack frames. Roughly 10⁴ simple frames fit in the default stack; deeper inputs need a loop or an explicit
ArrayDeque.
04 · see it
Play with it in 3D
Press Space to play, ←/→ to step, drag to move, ? for all shortcuts. Try your own input.
Call stack and heap, frame by frame
Boxes on the left are frames and their variables, spheres on the right are heap objects, arrows are references. Watch the array outlive the method that made it.
Pass-by-value: swap fails, arr[i] = … works
swap gets copies of two numbers. swapInArray gets a copy of an arrow to the caller's array. reassign repoints only its own arrow.
int overflow in 32 bits: the (lo + hi) / 2 bug
Each row is an int's 32 bits; the leftmost is the sign bit. Watch a carry flip it, then three ways to compute a midpoint safely.
Autoboxing: the Integer cache trap
From the Java Toolkit chapter. Boxed numbers are heap objects too, so == compares arrows: true for 127, false for 128.
05 · cost
Time & space, explained
| Lives in | Freed when | why | |
|---|---|---|---|
Local primitive (int i, long sum) | the method's stack frame | the method returns | The value itself sits in the frame, so popping the frame frees it in O(1). |
Local reference (int[] a, String s) | the frame (only the arrow) | the method returns | The frame stores just the reference. The object it points at is separate. |
Object or array (new int[n], new ListNode(5)) | the heap | the GC finds it unreachable | An object outlives the method that made it while any reference still reaches it. |
Static field (static int[] memo) | one copy for the whole class | practically never | Whatever a static field refers to stays alive for the whole run, across test cases. |
| Recursion of depth d | d frames on the stack | each call returns | Stack space is O(d) even when each call does O(1) work. |
| Time | Extra memory | why | |
|---|---|---|---|
| Pass an array or object to a method | O(1) | O(1) | Only the reference is copied, never the elements. |
int[] b = a; | O(1) | O(1) | A second arrow to the same array. |
a.clone() / Arrays.copyOf(a, n) | O(n) | O(n) | A new array; every element is copied. |
new int[n] | O(n) | O(n) | The JVM fills all n slots with 0 before you see the array. |
new ArrayList<>(path) snapshot | O(k) | O(k) | k = the path's current length, paid once per saved result. |
s.equals(t) vs s == t | O(n) vs O(1) | O(1) | equals compares characters (stopping at the first difference); == compares only references. |
| Recursion to depth d | (the calls' own work) | O(d) stack | One frame per call that is still waiting for its child to return. |
06 · java
Raw vs Collections Framework
Stack frames and heap objects
static int[] makeArray(int n) { int[] arr = new int[n]; // the array object lives on the heap for (int i = 0; i < n; i++) { arr[i] = (i + 1) * 10; } return arr; // hands back the reference, not a copy} static int sum(int[] a) { int total = 0; // a primitive: stored inside sum's frame for (int x : a) { total += x; } return total; // sum's frame is thrown away after this} static int run(int n) { int[] data = makeArray(n); int[] alias = data; // copies the arrow, not the array alias[0] = 99; // so data[0] is 99 now too return sum(data);}- Each call pushes a frame holding its parameters and locals;
returnpops it. That is whymakeArray'sino longer exists after it returns. new int[n]creates the array on the heap and hands you a reference. Returning it returns the reference; no elements are copied.int[] alias = datamakes a second arrow, not a second array. A write through either name is seen through both.- When the last arrow disappears the array is garbage. The GC reclaims it later; Java has no
freeordelete.
Pass-by-value: what a method can and cannot change
static void swap(int a, int b) { // a and b are COPIES of the caller's ints int t = a; a = b; b = t; // only the copies are swapped} static void swapInArray(int[] arr, int i, int j) { // arr is a copy of the REFERENCE int t = arr[i]; arr[i] = arr[j]; arr[j] = t; // writes into the caller's array object} static void reassign(int[] arr) { arr = new int[] {0, 0}; // points the local copy at a NEW array arr[0] = -1; // the caller never sees this array} static String demo(int x, int y) { swap(x, y); int[] nums = {x, y}; swapInArray(nums, 0, 1); reassign(nums); return x + " " + y + " " + Arrays.toString(nums);}static List<List<Integer>> prefixesBuggy(int[] nums) { List<List<Integer>> result = new ArrayList<>(); List<Integer> path = new ArrayList<>(); for (int x : nums) { path.add(x); result.add(path); // BUG: stores the SAME list object every time } return result; // every entry shows the final path} static List<List<Integer>> prefixes(int[] nums) { List<List<Integer>> result = new ArrayList<>(); List<Integer> path = new ArrayList<>(); for (int x : nums) { path.add(x); result.add(new ArrayList<>(path)); // snapshot: a new list holding today's contents } return result;}- Every argument is copied into a new parameter. For an
intthe copy is the number; for an array, list or node it is the reference. - Through a copied reference you can change the contents:
arr[i] = …,list.add(…),node.next = …. The caller sees it. - Assigning the parameter itself (
arr = new int[2],head = head.next) only moves the local arrow. The caller's variable does not move. - Backtracking classic:
result.add(path)stores the onepathobject again and again. Store a copy withnew ArrayList<>(path): O(k) per saved result.
Sharing a counter across recursive calls
static void countBuggy(int n, int count) { // every frame gets its own copy of count if (n == 0) return; count++; // lost as soon as this frame returns countBuggy(n - 1, count);} static void countInto(int n, int[] count) { // every frame shares ONE array on the heap if (n == 0) return; count[0]++; countInto(n - 1, count);} static int countReturned(int n) { // cleanest: return the answer up the stack return n == 0 ? 0 : 1 + countReturned(n - 1);}- An
int countparameter is copied into every frame, socount++vanishes when that frame returns. - Three versions that work: return the value up the stack (cleanest), share an
int[] {0}across frames, or use a field. - A
staticfield keeps its value between calls in the same run, so a count from one test case can leak into the next. Reset it at the start, or prefer an instance field.
== vs equals, and the String pool
static boolean[] stringChecks() { String a = "rahul"; // a literal: stored once in the String pool String b = "rahul"; // the same literal: the same pooled object String c = new StringBuilder("ra").append("hul").toString(); // built at run time: a new object return new boolean[] { a == b, // true: same object (thanks to the pool) a == c, // false: two objects, same text a.equals(c), // true: compares the characters };} static boolean[] arrayChecks() { int[] x = {1, 2, 3}; int[] y = {1, 2, 3}; return new boolean[] { x == y, // false: two array objects x.equals(y), // false: arrays keep Object.equals, which is == Arrays.equals(x, y), // true: element by element };}==compares what the variables hold: values for primitives, references for objects. It asks "same object?", not "same contents?".- String literals are interned (stored once), so
"rahul" == "rahul"is true. Strings built at run time (substring,StringBuilder, input) are new objects. - Arrays do not override
equals, sox.equals(y)is still identity. UseArrays.equalsfor 1D andArrays.deepEqualsfor 2D. - Boxed numbers have the same trap: the Integer cache sim above shows why
==works for 127 and fails for 128.
Arrays are objects: alias vs copy
static int[][] aliasVsCopy() { int[] original = {1, 2, 3}; int[] alias = original; // same array, two names int[] copy = original.clone(); // a new array with the same values (Arrays.copyOf works too) alias[0] = 100; // visible through original copy[1] = 200; // original is not affected return new int[][] {original, copy}; // [[100, 2, 3], [1, 200, 3]]}- An array is a heap object with a fixed
length, filled with0,falseornullwhen created. int[] alias = originalcopies the reference: one array, two names.clone()orArrays.copyOfbuilds a second array.- Copy before you modify when the caller still needs the original (for example, sorting a copy to keep the original indices).
2D arrays are arrays of row references
static int[][] sharedRowsBug(int rows, int cols) { int[][] grid = new int[rows][]; // an array of row REFERENCES, all null for now int[] row = new int[cols]; Arrays.fill(grid, row); // BUG: every grid[r] points at the SAME row grid[0][0] = 7; // so this "changes" every row return grid;} static int[][] separateRows(int rows, int cols) { int[][] grid = new int[rows][cols]; // rows separate row arrays, all zeros grid[0][0] = 7; return grid;} static int[][] deepCopy(int[][] grid) { int[][] copy = new int[grid.length][]; for (int r = 0; r < grid.length; r++) { copy[r] = grid[r].clone(); // grid.clone() alone would copy only the row references } return copy;}new int[r][c]is one array of r references, each pointing at its ownint[c]row.clone()andArrays.copyOfcopy one level only. For a grid you will modify (backtracking, simulations), clone every row.Arrays.fill(grid, row)puts the SAME row object in every slot. The bug stays hidden until the first write.
Primitive sizes, and when to reach for long
static long[] limits() { return new long[] { Byte.MAX_VALUE, // 127 (8 bits) Short.MAX_VALUE, // 32767 (16 bits) Character.MAX_VALUE, // 65535 (16 bits, unsigned: a char is a number) Integer.MAX_VALUE, // 2147483647, ~2.1e9 (32 bits) Long.MAX_VALUE, // 9223372036854775807, ~9.2e18 (64 bits) };} static long sumAll(int[] nums) { long total = 0; // 10^5 values of 10^9 each = 10^14: needs long for (int x : nums) total += x; return total;} static long areaWrong(int w, int h) { return (long) (w * h); // BUG: the int multiply overflowed before the cast} static long area(int w, int h) { return (long) w * h; // cast first, then multiply in 64 bits} static int checkedAdd(int a, int b) { return Math.addExact(a, b); // throws ArithmeticException instead of wrapping}- The JLS fixes the sizes on every machine:
byte8 bits,short16,char16 (unsigned),int32,long64. intruns from −2,147,483,648 to 2,147,483,647 (≈ ±2.1·10⁹).longreaches ≈ ±9.2·10¹⁸.- Reading constraints: values up to 10⁹ fit in an
int, but their sums and products usually do not. - Cast before the operation:
(long) a * b. Writing(long) (a * b)converts a number that is already wrong. Math.addExactandMath.multiplyExactthrowArithmeticExceptionon overflow, which is handy while debugging.doublekeeps 53 bits of precision: fine for averages, wrong for exact big integers or money. Store money aslongpaise.
Overflow and the (lo + hi) / 2 bug
static int wrapDemo() { return Integer.MAX_VALUE + 1; // wraps around to Integer.MIN_VALUE, silently} static int midBroken(int lo, int hi) { return (lo + hi) / 2; // lo + hi can pass 2^31 - 1 and turn negative} static int midSafe(int lo, int hi) { return lo + (hi - lo) / 2; // hi - lo never overflows when 0 <= lo <= hi} static int midShift(int lo, int hi) { return (lo + hi) >>> 1; // unsigned shift reads the wrapped bits correctly} static long sumAsLong(int lo, int hi) { return (long) lo + hi; // widen BEFORE adding: 64 bits hold any int sum}- int arithmetic wraps silently:
Integer.MAX_VALUE + 1 == Integer.MIN_VALUE. No exception, just a wrong number. - Joshua Bloch reported the
(low + high) / 2bug in the JDK's ownArrays.binarySearchin 2006. Today's JDK computes(low + high) >>> 1. lo + (hi - lo) / 2is the version to write in interviews: it is safe whenever0 ≤ lo ≤ hi, and easy to explain.
Autoboxing: what Integer costs
static long sumPrimitive(int n) { long total = 0; // 8 bytes inside the frame, no objects at all for (int i = 0; i < n; i++) total += i; return total;} static Long sumBoxed(int n) { Long total = 0L; // a Long OBJECT: each += unboxes, adds, then boxes a new Long for (int i = 0; i < n; i++) total += i; return total;} static int[] countsArray(int[] values, int maxValue) { int[] counts = new int[maxValue + 1]; // 4 bytes per counter, one object in total for (int v : values) counts[v]++; return counts;} static Map<Integer, Integer> countsMap(int[] values) { Map<Integer, Integer> counts = new HashMap<>(); // boxed keys, boxed values, one entry object each for (int v : values) counts.merge(v, 1, Integer::sum); return counts;}- An
intis 4 bytes stored in place. AnIntegeris a separate heap object with its own header, reached through a reference: several times the memory. Long total = 0L; total += i;unboxes, adds and boxes a freshLongon every iteration. Uselong.- For counts of small non-negative keys,
int[]beatsHashMap<Integer, Integer>: one object instead of a boxed key, a boxed value and an entry per key. - The
==trap on boxed values is covered in the Java Toolkit chapter (the Integer cache).
Recursion depth and StackOverflowError
static long sumTo(int n) { if (n == 0) return 0; // base case return n + sumTo(n - 1); // every call waits on the stack: depth n} static long sumToLoop(int n) { long total = 0; // one frame, reused n times for (int i = 1; i <= n; i++) { total += i; } return total;}- Each recursive call adds a frame until the base case returns. Depth n costs O(n) stack space even when each call does O(1) work.
- The default thread stack is 1 MB on 64-bit Linux and macOS (
-Xss). In our test (64-bit HotSpot, JDK 25/26 on Windows x64,-Xss1m) thissumTooverflowed after about 8,900 calls interpreted (-Xint) and 11,000–41,000 with the JIT on; your numbers will differ. sumToLoopdoes the same work in one frame. Recursion that just walks a line (a list, a counter) → write a loop. Branching recursion on deep inputs → an explicitArrayDequestack (see the Recursion chapter).- Java does not optimise tail calls, so even tail-recursive code grows the stack.
Garbage collection: the one habit DSA code needs
class ObjectStack<T> { private Object[] items = new Object[4]; private int size = 0; void push(T item) { if (size == items.length) items = Arrays.copyOf(items, size * 2); // old array becomes garbage items[size++] = item; } @SuppressWarnings("unchecked") T pop() { T top = (T) items[--size]; items[size] = null; // forget it, so the GC can reclaim the object return top; } int size() { return size; }}- The GC frees objects that nothing can reach. Reaching starts from live frames and static fields.
- Your own array-backed stack keeps popped objects alive through old slots unless you set them to
null.ArrayListandArrayDequealready clear removed slots. - Growing an array (
Arrays.copyOf) turns the old array into garbage. That is fine: the GC handles it. - Never call
System.gc()in a solution. It only suggests a collection, and a full collection is slow.
| call | does | cost |
|---|---|---|
Integer.MAX_VALUE / Integer.MIN_VALUE | 2³¹ − 1 and −2³¹: the int limits | O(1) |
Long.MAX_VALUE | 2⁶³ − 1 ≈ 9.2·10¹⁸ | O(1) |
(long) a * b | Multiply in 64 bits (cast first) | O(1) |
lo + (hi - lo) / 2 or (lo + hi) >>> 1 | Overflow-safe midpoint | O(1) |
Math.addExact / multiplyExact / toIntExact | Throw ArithmeticException instead of wrapping | O(1) |
a.clone() / Arrays.copyOf(a, n) | Shallow copy of an array | O(n) |
Arrays.equals(a, b) / Arrays.deepEquals(g, h) | Compare arrays element by element (deep for 2D) | O(n) |
s.equals(t) / Objects.equals(a, b) | Compare contents (the second is null-safe) | O(n) for strings |
new ArrayList<>(list) | Snapshot a list's current contents | O(n) |
A helper must change the caller's numbers
→ Return them (an int[] or a record), or change the contents of an array or object you passed
Primitive parameters are copies; contents behind a reference are shared.
Count something across recursive calls
→ Return the count up the stack, or a field / int[1]
An int parameter is copied per frame, so increments are lost.
Save a list that will keep changing
→ new ArrayList<>(list)
Storing the reference stores the future, mutated list.
A copy of an array you can change freely
→ a.clone() / Arrays.copyOf (row by row for 2D)
b = a copies only the reference.
Values, sums or products that can pass 2.1·10⁹
→ long, cast before the operation
int wraps silently; long holds up to 9.2·10¹⁸.
Compare strings or boxed numbers
→ equals / Objects.equals
== compares identity, not contents.
Recursion deeper than about 10⁴ levels
→ A loop, or an ArrayDeque as an explicit stack
Frames are limited by the thread stack; a deque's contents live on the heap.
07 · recognise
Spot the pattern
Constraints like nums[i] ≤ 10⁹ and you add or multiply values
long sums and (long) a * b
Binary search on indices or on an answer range near 2³¹
lo + (hi - lo) / 2, never (lo + hi) / 2
"Modify in place" and the method returns void
Change the array's contents through the reference you were given
A helper needs to give back two numbers
Return an int[] or a record; int parameters are copies
Collecting paths or subsets while backtracking
Save new ArrayList<>(path), not path
Recursion depth can reach 10⁴–10⁵ (long list, skewed tree, path graph)
Stack overflow risk: loop or explicit ArrayDeque
Comparing strings, Integers or arrays
equals / Arrays.equals, never ==
08 · traps & tricks
Mistakes to avoid, tricks to use
traps
- Expecting
swap(int a, int b)orcount++inside a method to change the caller's variables. - Reassigning a parameter (
arr = new int[n],head = head.next) and expecting the caller to see the new object. - Comparing
Strings,Integers or arrays with==: it compares references. result.add(path)in backtracking without copying: every saved entry is the same list.int mid = (lo + hi) / 2on large ranges, anintsum over 10⁵ values of 10⁹, or(long) (a * b).Arrays.fill(grid, row)orgrid.clone()and believing the rows are independent.- Recursing 10⁵ levels deep (a long linked list, a skewed tree) and getting
StackOverflowError. Long sum = 0LorInteger countin a hot loop: a new box on every update.- Keeping references you no longer need (popped slots, ever-growing caches): the GC cannot free them.
tricks
- Draw it: boxes for frames, circles for objects, arrows for references. Two arrows on one circle means a change through either is seen by both.
- Before submitting, look at every
intthat is summed or multiplied. Can it pass 2.1·10⁹? If unsure, make itlong. - Say it in the interview: "I pass the array reference, so the helper sorts it in place with O(1) extra space."
- When a helper must produce two numbers, return an
int[]or a record instead of fighting pass-by-value. - If recursion depth can reach about 10⁴, mention the stack risk yourself and offer the iterative version.
- Use
Objects.equals(a, b)when either side might benull. - Bas itna yaad rakho: primitives are copied, objects are shared through arrows.
09 · why & how
Questions you might be too shy to ask
Is Java pass-by-reference for objects?
No, and interviewers love this question. Java copies every argument's value. For an object variable that value is a reference, so the method gets a second arrow to the same object: it can change the object, but it cannot make the caller's variable point somewhere else. Real pass-by-reference (C++ int&) would allow that; Java never does.
How do I tell if a line changes the object or only moves my own arrow?
Look at the left side of the =. If it is the bare variable (arr = …, head = head.next), only your arrow moves; the caller sees nothing. If it goes through the variable (arr[i] = …, node.next = …, list.add(…)), the shared object changes and everyone sees it. Bas itna dekho: a dot or a [ ] before the change means the object itself is touched.
If objects are shared, why can't my method change a String I pass in?
Because a String object can never change: it is immutable. s = s + "x" inside the method builds a new String and moves only the local arrow. The caller's s still points at the old text. To give back a new string, return it.
What exactly is null, and why do I get NullPointerException?
null is a reference that points at no object. The variable exists, but there is nothing at the end of the arrow. Using it as an object (node.next, arr.length, s.equals(t)) throws NullPointerException. In linked lists and trees, check node != null before you touch node.next or node.left.
My code passes Run but fails Submit, and I used a static variable. Is that related?
Very likely. A judge may run many test cases in one program run (for LeetCode this is community-reported, not documented), and a static field keeps its value between them. Then a count or memo left over from test 1 corrupts test 2. Reset it at the start of the method, or make it a normal (instance) field or a local variable.
If the stack is small, can I put my variables on the heap instead?
Java decides for you: locals and parameters always live in the frame, objects always live on the heap. What you control is depth. A loop reuses one frame; recursion adds one per level. For very deep work, keep your own ArrayDeque, whose contents are heap objects.
Do I ever need to free memory in Java?
There is no free or delete. The GC reclaims an object once nothing reachable refers to it. Your only job is to stop referring to things you are done with. That rarely matters on LeetCode. It matters in long-running programs, like a cache that never drops old entries or an array slot that still points at a removed object.
Why does "abc" == "abc" print true if == is wrong for strings?
Both literals are the same pooled object: the JLS interns string literals. Strings you build at run time are new objects, so == turns false on exactly the hidden tests that build strings. equals is right in both cases.
Do I have to fill a new array with zeros? And why does my local int x; give a compile error?
new int[n] is already all 0 (false for boolean[], null for object arrays), so no fill loop is needed. Fields get the same defaults. Local variables do not: int x; x++; fails with "variable x might not have been initialized". Give every local a value before you read it.
I wrote long ans = a * b; with int a and b, and the answer is still wrong. Why?
Java computes the right side first, using the types of a and b. Both are int, so the multiply happens in 32 bits and wraps. Only the already-wrong result is then widened to long. Fix: long ans = (long) a * b; so the multiply itself runs in 64 bits.
How do I decide between int and long from the constraints?
Estimate the biggest value your variable can ever hold. Example: n ≤ 10⁵ values, each ≤ 10⁹, so the sum can reach 10⁵ × 10⁹ = 10¹⁴. That is far above 2.1·10⁹, so the sum needs long. Indices and counts up to 10⁵ are safe in int.
Why does Java wrap around instead of throwing on overflow?
Speed and simplicity. The JLS defines int arithmetic as 32-bit two's complement and says the integer operators never signal overflow, which matches what the CPU does natively. If you want a check, Math.addExact and Math.multiplyExact throw ArithmeticException.
My recursive solution passes small tests but crashes on big ones. Why?
Check the depth. A linked list or a skewed tree with 10⁵ nodes means 10⁵ waiting frames. A typical default stack (often 1 MB) holds only roughly 10⁴ simple ones; the exact limit depends on the JVM, its settings and how big each frame is. Make it iterative, or keep the recursion but bound its depth (balanced trees are only about log₂ n deep).
Does the recursion stack count in space complexity?
Yes. Every waiting call holds a frame, so recursion of depth d uses O(d) extra space even if you created no array. Example: recursive DFS on a skewed tree of n nodes is O(n) space, on a balanced tree O(log n). Say it in the interview, because interviewers check whether you noticed.
10 · practice
Interview problems
Practise this chapter on these
Each problem has one home page in the course; these use this chapter's idea too. The note says what to look for.
- #704Binary SearchEasyin Binary Search
Write the midpoint as
lo + (hi - lo) / 2and say why out loud:lo + hican overflow. - #69Sqrt(x)Easyin Big-O
mid * midoverflows anintonce mid passes 46,340. Square inlong, or comparemid <= x / mid. - #50Pow(x, n)Mediumin Recursion
n = Integer.MIN_VALUEhas no positiveintpartner, so-noverflows. Widen tolongfirst. - #283Move ZeroesEasyin Two Pointers
The method returns
void, yet the caller sees the result: the array is changed through the copied reference. - #78SubsetsMediumin Backtracking
The snapshot trap in its natural habitat: add
new ArrayList<>(path), neverpathitself. - #206Reverse Linked ListEasyin Linked Lists
Pure reference rewiring with three local arrows and no new nodes. The recursive version uses one stack frame per node.
- #104Maximum Depth of Binary TreeEasyin Tree Recursion
Recursion depth equals tree height: a skewed tree of 10⁴ nodes means 10⁴ frames.
11 · revise
The whole chapter on one screen
10 cards, about a minute. Read them the night before an interview, or right after the chapter to lock it in.
- what it is
Each call gets a frame on the stack (parameters + locals). Every
newobject or array lives on the heap. Variables hold references: arrows to objects. - what it is
Java is always pass-by-value. For an object, the value copied is the reference: a method can change the object, never the caller's variable.
- use it when
Use
longwhen sums or products can pass ~2.1·10⁹: many big values,a * babove 46,340, prefix sums, mod 10⁹+7 math. - template
Safe midpoint:
int mid = lo + (hi - lo) / 2;orint mid = (lo + hi) >>> 1; - cost
Passing an array or list to a method is O(1): one reference is copied. Copying it (
clone,new ArrayList<>(list)) is O(n). - cost
Recursion depth d uses O(d) stack. A default 1 MB stack holds roughly 10⁴ simple frames, then
StackOverflowError. - trap
==on objects compares references. UseequalsforString/IntegerandArrays.equalsfor arrays. Literals only look safe because they are pooled. - trap
result.add(path)stores the same list every time. Snapshot it:result.add(new ArrayList<>(path)). - trap
(long) (a * b)is too late: the int multiply already wrapped. Write(long) a * b. - pro tip
Garbage = unreachable. In your own array-backed stack, set
items[size] = nullafter a pop so the GC can free the object.
12 · recall
Flip, remember, repeat
Where do a method's local int and its int[] arr = new int[n] live?
Question 1: Where do a method's local int and its int[] arr = new int[n] live?
13 · check yourself
Quick quiz
`int x = 1, y = 2; swap(x, y);` where swap exchanges its two int parameters. What are x and y afterwards?
`void f(int[] a) { a[0] = 7; a = new int[] {9}; }` is called with `arr = {1, 2}`. What is arr afterwards?
`String a = "hi"; String b = new StringBuilder("h").append("i").toString();` What are `a == b` and `a.equals(b)`?
lo = 1,500,000,000 and hi = 2,000,000,000 are ints. What does `(lo + hi) / 2` give?
Which of these lives on the heap?
Which line correctly saves the current backtracking path?
Which expression multiplies the ints w and h without overflow?
14 · learn more
A second teacher, if you want one
- ▶ videoIntroduction to Programming - Types of Languages, Memory Management ↗Kunal Kushwaha · EnglishDraws the stack, the heap and the reference arrow between them, so you can see what `new` really does before arrays and objects get shared.
- ◈ visualizerVisualize Java Code - Java Visualizer, Tutor, and Debugger ↗Python Tutor (Philip Guo) · EnglishPaste a small Java program and step through it line by line, watching stack frames and heap objects appear. It's the fastest way to check your mental picture.
- ✎ articleStack Memory and Heap Space in Java ↗Baeldung · EnglishA short read on what lives where, with a worked example, plus why StackOverflowError and OutOfMemoryError are two different problems.