Learning/Cheatsheet/Java Syntax Mechanics
11 min read

03 — Java Syntax Mechanics

Java-specific friction that costs minutes and correctness points. These are the mechanics the Part 2 question files refer back to.

Each section explains why the rule exists — rules you understand are the ones you remember at 2pm in an interview.

Comparators — pointer

Comparators, Comparable, multi-key sorting, sorted collections of custom objects, and sorting maps by key or value all live in 04 — Ordering: Comparable, Comparator & Sorting. Two mechanics from there are repeated below because they are language traps rather than sorting technique.

The subtraction trap

(a, b) -> a - b is wrong in general, because of integer overflow.

Java's int holds roughly −2.1 billion to +2.1 billion. Exceed the range and it silently wraps to the other end:

Java
a = Integer.MIN_VALUE;   // -2147483648
b = 1;
a - b;                   // should be -2147483649 — doesn't fit
                         // wraps to +2147483647, a POSITIVE number

The comparator now claims a is greater than b, which is backwards. The sort produces silently wrong output — no exception, no warning.

Use Integer.compare(a, b) or Comparator.comparingInt. They compare rather than subtract, so nothing can overflow.

a - b is fine when you know the values are small and non-negative (array indices, character codes). If you use the short form deliberately, say so aloud — "these are all small positive values so subtraction is safe here" reads as expertise rather than carelessness.

Primitive arrays can't take a comparator

Java
Arrays.sort(intArray, (a, b) -> b - a);    // DOES NOT COMPILE

There is no Arrays.sort(int[], Comparator) overload. Comparators work on objects, and int is a primitive. To sort an int[] descending you must box:

Java
Integer[] boxed = Arrays.stream(nums).boxed().toArray(Integer[]::new);
Arrays.sort(boxed, (a, b) -> b - a);

But int[][] does work with a comparator, because its elements are int[] — which are objects:

Java
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);    // fine

That asymmetry is why interval problems sort cleanly while "sort this int array descending" doesn't.

Characters and strings

Characters are numbers

A char in Java is a 16-bit integer holding a character's numeric code. 'a' is 97, 'b' is 98, '0' is 48. Because they're numbers, you can do arithmetic — and that's the basis of every alphabet-indexing trick.

Java
int idx = c - 'a';                 // 'a'->0, 'b'->1, ... 'z'->25
char c = (char) ('a' + idx);       // back again — the cast is REQUIRED
int d = c - '0';                   // digit character -> its numeric value
char digit = (char) ('0' + d);

Why c - 'a' works: if c is 'd' (100) and 'a' is 97, then c - 'a' is 3 — 'd' is the 3rd letter counting from 0. That gives you a perfect index into an int[26].

Why the cast is needed going back: 'a' + idx promotes to int, and Java won't narrow an int to a char implicitly (it could lose data). The explicit cast says "I know this fits."

Java
Character.isLetterOrDigit(c);
Character.isDigit(c);
Character.isLetter(c);
Character.toLowerCase(c);
Character.toUpperCase(c);

Strings

Java
s.length();                        // METHOD — strings
arr.length;                        // FIELD — arrays, no parentheses
list.size();                       // METHOD — collections

s.charAt(i);
s.toCharArray();                   // O(n) copy — do it once, outside loops
s.substring(i, j);                 // [i, j) — end exclusive. O(j-i), it COPIES
s.indexOf("x");
s.contains("x");
s.equals(t);                       // NEVER use == for strings
s.split("\\s+");                   // takes a regex: "." must be escaped as "\\."
s.trim();
s.toLowerCase();
String.valueOf(x);
String.join(",", list);
new String(charArray);

Three cost traps

1. substring copies. In Java 7+, substring allocates a new string and copies the characters. Inside a nested loop that turns an O(n²) algorithm into O(n³) without you noticing. Carry (start, end) indices instead where possible.

2. s += c in a loop is O(n²). Strings are immutable, so every += builds a whole new string by copying the old one. Always StringBuilder (02).

3. == compares references, not content.

Java
String a = "hello";
String b = "hello";
a == b;                  // true — but only by accident

String c = new String("hello");
a == c;                  // FALSE — different objects, same content
a.equals(c);             // true — correct

Java keeps a pool of string literals, so two identical literals point at the same object and == appears to work. Computed strings aren't pooled, so the same code fails on real input. Always use .equals.

Boxing, equality, and null

What boxing is

Java has two parallel worlds:

  • Primitivesint, char, double, boolean. Fast, stored directly, cannot be null.
  • Wrapper objectsInteger, Character, Double, Boolean. Slower, stored as references, can be null.

Collections can only hold objects, so List<int> is illegal — you need List<Integer>. Java converts automatically (autoboxing in, unboxing out), which mostly works and occasionally bites hard.

The Integer cache trap

Java
Integer a = 127, b = 127;
a == b;                 // true

Integer c = 128, d = 128;
c == d;                 // FALSE
c.equals(d);            // true

Java pre-creates Integer objects for −128 to 127 and reuses them, so == (which compares references) happens to work in that range. Above 127 you get distinct objects and == fails.

This bites in Map<Integer, Integer> and List<Integer> comparisons, and it passes small tests before failing on large ones — the worst kind of bug.

The rule: == for primitives, .equals() for wrappers.

When both sides are boxed and you want numeric comparison, call .intValue() on one side to force primitive comparison:

Java
if (window.get(c).intValue() == need.get(c).intValue()) { ... }   // Minimum Window Substring

Null unboxing

Java
Map<String, Integer> m = new HashMap<>();
int v = m.get("missing");               // NullPointerException
int v = m.getOrDefault("missing", 0);   // correct

get returns null for an absent key. Assigning null to an int means calling .intValue() on null. Always getOrDefault.

List<Integer>.remove is ambiguous

Java
List<Integer> list = new ArrayList<>(List.of(10, 20, 30));
list.remove(1);                       // removes INDEX 1 -> removes the value 20
list.remove(Integer.valueOf(20));     // removes the VALUE 20

remove(int) and remove(Object) both exist. A bare 1 is an int, so Java picks the index version. Wrap it to force the value version.

Integer overflow

Java
Integer.MAX_VALUE;   //  2,147,483,647
Integer.MIN_VALUE;   // -2,147,483,648
Long.MAX_VALUE;      //  about 9.2 × 10^18

Java wraps silently on overflow — no exception. Three places it matters across the 150:

1. Binary search midpoint

Java
int mid = (lo + hi) / 2;              // WRONG: lo + hi can overflow
int mid = lo + (hi - lo) / 2;         // correct

If lo and hi are both near 2 billion, their sum wraps negative and mid lands outside the array. The safe form only ever computes a difference, which stays in range. This is a famous bug that sat in the JDK's own binary search for years.

2. Accumulating products or sums

Java
long product = a * b;                 // WRONG: a * b is computed as int, THEN widened
long product = (long) a * b;          // correct: cast first, multiply in long

Java evaluates a * b using the operand types. Both are int, so the multiplication happens in int and overflows before the assignment widens it. Casting one operand first forces the whole expression into long.

3. Negating Integer.MIN_VALUE

Java
-Integer.MIN_VALUE == Integer.MIN_VALUE;    // true (!)
Math.abs(Integer.MIN_VALUE);                // returns Integer.MIN_VALUE, still negative

The int range is asymmetric: it goes down to −2,147,483,648 but only up to +2,147,483,647. So the positive counterpart of MIN_VALUE does not exist, and negating it wraps back to itself.

The fix is to widen before negating:

Java
long n = Math.abs((long) exponent);   // Pow(x, n)

Interviewers use n = Integer.MIN_VALUE as a test case specifically to check this.

Sentinel values

When you need "infinity" for a minimization:

Java
int best = Integer.MAX_VALUE;
best = Math.min(best, candidate);

But be careful adding to it — Integer.MAX_VALUE + 1 wraps to MIN_VALUE, turning your "worst possible" into "best possible". Two fixes:

Java
if (dp[i] != Integer.MAX_VALUE) dp[i] + 1;     // guard before adding
int[] dp = new int[n]; Arrays.fill(dp, amount + 1);   // or use a large FINITE sentinel

The second is what Coin Change does — amount + 1 is bigger than any real answer but small enough that adding to it is safe.

Array ↔ List conversions

Constant friction. The four you need:

Java
// int[] -> List<Integer>
List<Integer> list = Arrays.stream(nums).boxed().collect(Collectors.toList());

// List<Integer> -> int[]
int[] arr = list.stream().mapToInt(Integer::intValue).toArray();

// T[] -> List<T>  (fixed size, backed by the array)
List<String> view = Arrays.asList(strArr);

// List<T> -> T[]
String[] out = list.toArray(new String[0]);

// List<List<Integer>> -> int[][]
int[][] res = list.stream()
                  .map(l -> l.stream().mapToInt(Integer::intValue).toArray())
                  .toArray(int[][]::new);

Two traps

Arrays.asList(intArray) doesn't do what you expect.

Java
int[] nums = {1, 2, 3};
List<int[]> wrong = Arrays.asList(nums);      // a list of ONE element: the array itself

asList takes varargs of objects. An int[] is a single object, so you get a one-element list. Autoboxing applies to individual values, never to whole arrays. Use the stream form.

Arrays.asList and List.of are structurally immutable.

Java
List<Integer> list = Arrays.asList(1, 2, 3);
list.add(4);                                  // UnsupportedOperationException
List<Integer> ok = new ArrayList<>(Arrays.asList(1, 2, 3));   // now mutable

Arrays.asList returns a view of the array — you can set an element but not change the size. List.of is fully immutable. Wrap in new ArrayList<>(...) when you need to mutate.

2-D arrays

Java
int[][] grid = new int[rows][cols];       // all zeros
int rows = grid.length;                   // number of rows
int cols = grid[0].length;                // number of columns — guard rows > 0 first

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

A Java 2-D array is really an array of arrays, so rows are independent objects. Rows can in principle have different lengths (a "jagged" array); assume rectangular unless told otherwise.

Java
int[] rowCopy = grid[i].clone();          // copies one row

The direction vector

Every grid traversal in the 150 uses this. Instead of writing four near-identical blocks for up/down/left/right, store the offsets and loop:

Java
int[][] DIRS = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};   // right, left, down, up

for (int[] d : DIRS) {
    int nr = r + d[0];                    // new row
    int nc = c + d[1];                    // new column
    if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue;   // off the grid
    // ... visit (nr, nc)
}

The bounds check must come before using nr/nc as indices, or you get ArrayIndexOutOfBoundsException. Checking all four conditions in one if with continue is the compact idiom.

Equivalent form with fewer allocations:

Java
int[] dr = {0, 0, 1, -1}, dc = {1, -1, 0, 0};
for (int k = 0; k < 4; k++) {
    int nr = r + dr[k], nc = c + dc[k];
}

For 8 directions (including diagonals), use all pairs except {0,0}.

Recursion mechanics

Java has no output parameters for primitives

Java passes arguments by value — the function gets a copy. So a recursive helper cannot update an int belonging to its caller:

Java
void helper(int count) { count++; }       // modifies the COPY; caller sees nothing

Three fixes, in order of interview preference:

Java
// 1. Instance field — cleanest to read
private int best = 0;
public int solve(TreeNode root) {
    best = 0;                             // reset! the field may hold a previous run's value
    dfs(root);
    return best;
}

// 2. A one-element array as a mutable box
int[] best = new int[1];
dfs(root, best);                          // helper does best[0] = ...

// 3. Return the value and combine at each level  <- prefer when it composes
private int dfs(TreeNode node) {
    ...
    return valueForMyParent;
}

Option 2 works because the array reference is copied, but both copies point at the same array — so writes through it are visible to the caller.

Pattern 3 combined with a field is the standard tree shape: return one thing to the parent, record a different thing globally. That's Diameter of Binary Tree and Binary Tree Maximum Path Sum (12).

Remember to reset the field at the start of the public method. Leaving stale state between calls is a real bug on platforms that reuse the object across test cases.

Stack depth

Each pending recursive call occupies a frame on the call stack, and the JVM's default stack holds roughly 10,000 frames. Recursing deeper throws StackOverflowError.

That's fine for a balanced tree (depth ~`log n`). It is not fine for:

  • a degenerate skewed tree (depth = n),
  • a linked list of length 10^5,
  • DFS on a 10^5-node graph.

When n ≥ 10^5, mention the iterative alternative — it shows you're thinking about real limits, not just asymptotics.

Small things that save time

Java
Math.max(a, b);  Math.min(a, b);  Math.abs(x);
Math.pow(a, b);                    // returns double — cast if you need an int
Math.sqrt(x);
Math.floorDiv(a, b);               // rounds toward -infinity; / truncates toward zero
(a + b - 1) / b;                   // integer CEILING division, for non-negative a and b

Integer.parseInt(s);
Integer.toBinaryString(x);
Integer.bitCount(x);               // count of 1-bits
Integer.compare(a, b);

Why (a + b - 1) / b is a ceiling: integer division truncates down. Adding b - 1 first pushes any non-zero remainder up over the next multiple. For a = 7, b = 3: (7 + 2) / 3 = 3, which is ceil(7/3). Use this rather than Math.ceil on doubles — floating point rounding causes off-by-one bugs on large values.

Math.floorDiv vs /: for -7 / 2 Java gives -3 (truncating toward zero), while Math.floorDiv(-7, 2) gives -4 (flooring). Matters whenever negative coordinates appear.

Records (Java 16+)

Make pair-carrying readable when the language level allows:

Java
record Pair(int node, int cost) {}
PriorityQueue<Pair> pq = new PriorityQueue<>(Comparator.comparingInt(Pair::cost));
pq.offer(new Pair(3, 10));

Confirm the Java version before relying on them. int[] pairs work everywhere and are what most interviewers expect — slightly less readable, universally safe.