Java Coding Interview Pitfalls: Ten Bugs Hidden in Plain Sight

Integer ==only works inside the JDK cache range −128 to 127; always use.equals()to compareIntegerobjects- Comparator subtraction overflows with extreme values; use
Integer.compare(a, b)in everyPriorityQueuecomparator - Autoboxing NPE strikes when
get()returns null and is unboxed to a primitive; usegetOrDefaultto stay safe Arrays.asListreturns a fixed-size view that throwsUnsupportedOperationExceptiononadd()orremove()Stack<T>carries synchronized overhead inherited fromVector; useArrayDeque<T>for both stack and queue operations- Java
%returns the sign of the dividend, not a value in[0, m); useMath.floorModfor correct modular arithmetic Math.abs(Integer.MIN_VALUE)returns a negative number; cast tolongbefore callingMath.abson arbitrary input
You know the algorithm. You've got the data structure. You trace through the logic and it checks out. Then you submit, and the test case with negative numbers fails, or the heap returns elements in the wrong order, or an NPE appears on a line that contains no null at all.
Most Java gotchas share one property: they're invisible until a specific input hits them. Positive-only numbers never trigger the comparator overflow. Small integers never expose the == trap. Edge cases in interview problems are designed to find exactly these silent bugs. The interviewer has seen each one of these many times. You, probably, have not.
This guide covers the ten Java interview pitfalls that bite candidates most often, with the exact fix for each.
Integer == Works Right Up Until It Doesn't
Java caches Integer objects for values between -128 and 127. Inside that range, == returns true by coincidence, because both variables point to the same cached object. Outside that range, two Integer variables holding the same value are different objects, and == tells you they're not equal.
Integer a = 127, b = 127; System.out.println(a == b); // true (cached) Integer x = 128, y = 128; System.out.println(x == y); // false (two separate objects) System.out.println(x.equals(y)); // true
Works for every value your hand-crafted test cases use. Fails for the values the interview test suite prefers, which are larger than 127.
Always use .equals() to compare Integer objects. The cache is a JDK implementation detail, guaranteed for the range -128 to 127 by the spec, but you shouldn't depend on it semantically. This surfaces most often when comparing keys retrieved from a Map<Integer, Integer> or checking if two values in a frequency map are equal.
Your Lambda Comparator Will Silently Corrupt the Heap
The (a, b) -> a - b shorthand looks like a perfectly valid ascending comparator. It overflows.
// Wrong: overflows when a = Integer.MAX_VALUE and b = -1 PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> a - b); // Correct PriorityQueue<Integer> pq = new PriorityQueue<>(Integer::compare); // Max-heap PriorityQueue<Integer> maxPq = new PriorityQueue<>(Collections.reverseOrder());
If a = Integer.MAX_VALUE and b = -1, then a - b overflows and wraps to a large negative number. The comparator now says a < b. The heap silently builds wrong. Your top-K or median solution returns garbage. Nothing throws an exception. You have no idea.
Use Integer.compare(a, b) instead of a - b. For descending order, reverse the arguments: Integer.compare(b, a). This is the interview equivalent of discovering your parachute has a one-in-a-billion failure rate, during the billion-th jump.
The NPE That Doesn't Look Like a Null Check
Autoboxing converts between int and Integer automatically. Unboxing (going the other way) calls .intValue() on the wrapper object. If that object is null, you get an NPE on a line that looks like plain arithmetic. No null in sight.
Map<String, Integer> freq = new HashMap<>(); int count = freq.get("missing"); // NPE: null unboxed to int
The fix is getOrDefault:
int count = freq.getOrDefault("missing", 0); // safe // When the value is a mutable object, computeIfAbsent avoids creating it on every call freq.computeIfAbsent("missing", k -> new ArrayList<>());
Whenever you unbox a wrapper type retrieved from a collection, treat null as a real possibility. getOrDefault handles the simple case. computeIfAbsent is cleaner when building adjacency lists or grouping problems where the value is itself a collection. The NPE materializes anyway, is the thing. Java just does that.
Arrays.sort on int[] Cannot Take a Comparator
Arrays.sort(int[]) uses dual-pivot Quicksort. It's not stable. And the overload that takes a custom Comparator does not exist for primitive arrays.
int[] arr = {3, 1, 4}; Arrays.sort(arr, (a, b) -> b - a); // compile error
To sort with a custom comparator, box the array first:
Integer[] arr = {3, 1, 4}; Arrays.sort(arr, Collections.reverseOrder());
The compiler telling you "no" here is a genuine gift. Say thank you. If you only need descending order for a primitive array, sorting ascending and reversing with a two-pointer swap is faster and less error-prone than converting to Integer[] and back.
Arrays.asList Returns a Trap, Not a List
Arrays.asList() does not return a java.util.ArrayList. It returns a fixed-size view backed by the original array. You can set elements. But add() and remove() throw UnsupportedOperationException at runtime.
List<String> list = Arrays.asList("a", "b", "c"); list.add("d"); // UnsupportedOperationException list.remove(0); // UnsupportedOperationException
Wrap it to get a mutable list:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
This compiles cleanly and crashes mid-interview. The most common version: you initialize a list inline with Arrays.asList, pass it to a helper method that calls add, and the stack trace points to a line inside the helper that looks completely innocent. Interviewers have watched this happen many times and do not always tell you what's wrong.
Stack Is the Wrong Choice. Use ArrayDeque.
java.util.Stack extends Vector, which means every push and pop acquires a synchronized lock you never asked for. Vector is old enough to be deprecated in spirit. The JDK documentation explicitly says to use Deque implementations instead.
// Legacy and carries synchronized overhead you don't need Stack<Integer> stack = new Stack<>(); // Correct Deque<Integer> stack = new ArrayDeque<>(); stack.push(1); stack.pop(); stack.peek(); // As a queue Deque<Integer> queue = new ArrayDeque<>(); queue.addLast(1); queue.removeFirst();
ArrayDeque is the right structure for both stack and queue operations. It's faster than Stack and faster than LinkedList for queue use because it avoids per-node heap allocation. Declare it as Deque<T> to keep the interface general.
Java's % Returns a Negative Number for Negative Inputs
Python's % always returns a non-negative result. Java's % takes the sign of the dividend. Python thought about what you wanted. Java thought about what the CPU does.
System.out.println(-13 % 3); // -1, not 2 System.out.println(-1 % 5); // -1, not 4
This corrupts circular array indexing, ring buffer wraparound, and any modular arithmetic expecting a positive result.
// Wrong for negative inputs int index = offset % size; // Correct int index = Math.floorMod(offset, size); // always in [0, size) // If Math.floorMod is not available (pre-Java 8): int index = ((offset % size) + size) % size;
Math.floorMod was added in Java 8 and gives the mathematically correct floored modulo. Use it whenever you're wrapping indices or doing cyclic arithmetic. This is one of the most common cross-language differences that candidates trip on after switching from Python, and it never announces itself.
Math.abs(Integer.MIN_VALUE) Is Still Negative
Integer.MIN_VALUE is -2,147,483,648. Its mathematical absolute value is 2,147,483,648. But Integer.MAX_VALUE is only 2,147,483,647. The result can't be represented as an int, so it overflows and wraps back to Integer.MIN_VALUE.
System.out.println(Math.abs(Integer.MIN_VALUE)); // -2147483648 System.out.println(Integer.MIN_VALUE == -Integer.MIN_VALUE); // true
The absolute value of a number is negative. This is Java. The same applies to Long.MIN_VALUE. This surfaces in problems where you negate values, compute absolute differences, or call Math.abs on arbitrary input.
// Safe: cast to long first long safe = Math.abs((long) n);
If your code handles arbitrary integers and applies Math.abs, the all-negative case where the minimum value appears will return a wrong answer without any exception. Cast to long before calling Math.abs on an int.
char Arithmetic Promotes to int. StringBuilder Noticed.
char in Java is an unsigned 16-bit integer. Add two char values or add char to int, and the result is int. This is correct arithmetic, but it bites in two specific patterns.
// Computing a mid-character: result is int, needs explicit cast char mid = (char) (('A' + 'Z') / 2); // Shifting a character: must cast back to char int shift = 3; char shifted = (char) ('a' + shift); // The StringBuilder trap StringBuilder sb = new StringBuilder(); sb.append('a' + shift); // appends the int 100, not the char 'd' sb.append((char) ('a' + shift)); // correct
StringBuilder.append(int) and StringBuilder.append(char) are different overloads. When the argument is 'a' + shift, the expression has type int and the integer value is appended as a string of digits. Java's type promotion rules exist for good historical reasons. None of those reasons will occur to you at the moment you need them.
String == Passes Every Test You Ran by Hand
String literals are interned. Two literal strings with identical content usually share the same object, so == appears to work during casual testing. Strings built at runtime are not interned by default.
String a = "hello"; String b = "hello"; System.out.println(a == b); // true (interned literals) String c = new String("hello"); System.out.println(a == c); // false System.out.println(a.equals(c)); // true String d = "hel" + "lo"; // compile-time constant: interned String e = "hel"; String f = e + "lo"; // runtime concatenation: NOT interned System.out.println(a == f); // false
Always compare strings with .equals(). In a sorted-order context, use .compareTo(). The == form passes on every test case you write by hand and fails on every test the interviewer runs. Inputs built from substring, valueOf, or concatenation are not interned. Interviewers have seen this bug many times.
Java Coding Interview Pitfall Quick Reference
| Pitfall | Safe replacement |
|---|---|
Integer == Integer | .equals() |
(a, b) -> a - b comparator | Integer.compare(a, b) |
| Unboxing null from map | getOrDefault(key, 0) |
Arrays.sort(int[], comparator) | Box to Integer[] first |
Arrays.asList(...) then add() | new ArrayList<>(Arrays.asList(...)) |
Stack<T> | ArrayDeque<T> |
n % m with negative n | Math.floorMod(n, m) |
Math.abs(Integer.MIN_VALUE) | Math.abs((long) n) |
sb.append('a' + shift) | sb.append((char)('a' + shift)) |
String == String | .equals() |
Three More Worth Knowing
int overflow in midpoint calculation: (lo + hi) / 2 overflows when both values are large. This was the bug in Java's own Arrays.binarySearch for nine years, documented by Joshua Bloch in 2006. The safe form is lo + (hi - lo) / 2, or for unsigned arithmetic in Java, (lo + hi) >>> 1.
LinkedHashMap for insertion-order iteration: HashMap does not guarantee iteration order. If a problem requires you to return elements in insertion order (LRU cache, most recent k elements), LinkedHashMap maintains it without extra bookkeeping.
ConcurrentModificationException when modifying during iteration: removing elements from a List or Map while iterating with a for-each loop throws at runtime. Use Iterator.remove(), removeIf(), or collect elements to a separate list and remove them after.
These are the bugs that show up on the tenth test case, not the first. Knowing the rules helps. The deeper skill is catching your own mistakes while you're also explaining your approach out loud, under time pressure, with someone watching. If you want to build that muscle, SpaceComplexity runs voice-based mock interviews with rubric feedback that covers exactly this combination of technical correctness and communication clarity.
For more on Java interview mechanics, the Java for Coding Interviews guide covers the full standard library from collections to sorting. The integer overflow post digs into how silent wrapping can corrupt results across all languages. The coding interview edge cases guide has the six-category checklist for finding bugs before the test suite does. And if you're debating which language to use, best language for coding interviews covers the fluency tradeoff honestly.