Kotlin Coding Interview Gotchas: The Footguns Nobody Warns You About

May 29, 20269 min read
dsaalgorithmsinterview-prepkotlin
TL;DR
  • mutableMapOf() returns a LinkedHashMap; use HashMap or TreeMap when ordering or performance matter
  • Int overflow is silent in Kotlin — declare accumulators as Long before you start accumulating
  • .sorted() returns a new list while .sort() mutates in-place; discarding .sorted() is a silent bug
  • Comparator subtraction overflows near Int.MIN_VALUE; always use compareTo or compareBy
  • data class equals() only sees primary constructor properties — body var fields are invisible to equality and copy()
  • Arrays require contentEquals() for structural comparison; == compares by reference
  • tailrec works only for a single tail call — tree recursion still needs an explicit ArrayDeque stack

Kotlin feels safe. Null checks built in, extension functions everywhere, syntax that makes Java look like boilerplate soup. You pick it for your coding interview, feel confident, then hit minute 20. Your algorithm is solid. The output is -727379968.

No null pointer. No exception. Just your career, quietly on fire.

If you're still deciding whether Kotlin is the right pick, see the Kotlin for coding interviews guide first. If you've already committed, here are the traps that bite mid-interview with zero warning.


The Collection You Get Is Not Always the Collection You Think

listOf() returns a read-only view, not an immutable object. You can't call .add() on it, but it reflects mutations to the underlying data if you hold a mutable reference elsewhere. For interviews, this mostly matters when you expose a list from a helper and accidentally mutate it from another path.

mutableMapOf() returns a LinkedHashMap, not a HashMap. Insertion order is preserved by default. That's usually fine, but if you need sorted keys or no ordering overhead, construct the type directly. Nobody warns you about this in the prep videos, and you'll spend a confusing few minutes wondering why your map iteration has opinions.

val map = mutableMapOf<String, Int>() // LinkedHashMap, insertion order preserved val hashMap = HashMap<String, Int>() // explicit HashMap val treeMap = TreeMap<String, Int>() // sorted by key, O(log n) ops

Decision table:

NeedType
Fast lookup, no orderingHashMap
Sorted keys, floor/ceilingTreeMap
Insertion-order iterationLinkedHashMap / mutableMapOf()
Min-heapPriorityQueue()
Max-heapPriorityQueue(reverseOrder())
Stack or queueArrayDeque

Int Has a 32-Bit Ceiling. Your Answer Might Not.

Kotlin's Int is 32-bit signed. Range: roughly ±2.1 billion. Sum a list of large integers, multiply two moderately large values, or accumulate prefix sums on an array with 10^5 elements each up to 10^5, and you blow past this silently.

val a = 1_000_000 val b = 1_000_000 println(a * b) // -727379968. Overflow. println(a.toLong() * b) // 1000000000000L. Correct.

The compiler does not warn you. The test cases with small inputs pass. You move on. Then the 5th test case explodes and you lose five minutes trying to understand why your logic is wrong, when actually your types are wrong.

Declare accumulator variables as Long before you accumulate, not after. Converting at the end doesn't help because the overflow already happened.

var sum = 0L for (n in nums) sum += n val prefix = LongArray(n + 1)

Any problem where values reach 10^5 and you multiply or sum them needs Long. Kotlin's type inference makes this feel safer than it is.


.sort() Mutates. .sorted() Doesn't.

Both are called with the same dot notation, which is why this one lands mid-contest.

val nums = mutableListOf(3, 1, 2) nums.sort() // mutates in-place, returns Unit val sorted = nums.sorted() // returns NEW list, original unchanged

Calling .sorted() without capturing the return value leaves your list unsorted. No compiler error. No crash. Just wrong output on exactly the test case with the edge condition you didn't check manually. The same trap exists for .reversed() vs .reverse().

nums.sorted() // result discarded, silent bug nums.sort() // correct in-place sort val result = nums.sorted() // correct copy

For arrays: array.sort() mutates in-place. array.sortedArray() returns a new copy.


Never Subtract in a Comparator

Using subtraction to compare integers gives silent wrong behavior whenever values span the int range.

// Wrong: overflows when values span the int range val pq = PriorityQueue<Int> { a, b -> a - b } // Right val pq = PriorityQueue<Int> { a, b -> a.compareTo(b) }

Int.MIN_VALUE - 1 wraps to Int.MAX_VALUE. If one value is -2 billion and another is +2 billion, the comparator lies about ordering and your heap is just a pile of chaos. Use compareTo or compareBy, always. It's the same footgun in Java (see Java coding interview pitfalls), but Kotlin's lambda syntax makes the subtraction version easy to type without thinking.

For max-heaps:

val maxHeap = PriorityQueue<Int>(reverseOrder()) val pq = PriorityQueue<Task>(compareByDescending { it.priority })

== Is Structural. === Is Reference.

Kotlin fixed Java's == footgun: == calls equals() by default. === checks reference identity, which is what Java's == does on objects.

Mostly good news. The trap is with data class. A data class generates equals() and hashCode() from its primary constructor properties. Use one as a map key and mutate a var field, and the hash code changes. The key vanishes.

data class Point(var x: Int, var y: Int) val map = HashMap<Point, String>() val p = Point(1, 2) map[p] = "hello" p.x = 99 // hash code just changed println(map[p]) // null. Key is lost.

Use val in data class primary constructors if instances will be used as map keys. This is the kind of bug that passes 9 out of 10 test cases and fails on the one where the interviewer added a mutation step.

One more: properties declared in the body, outside the primary constructor, are invisible to equals(), hashCode(), and copy().

data class User(val id: Int) { var name: String = "" // NOT included in equals or copy } User(1) == User(1) // true, even if name differs User(1).copy() // name resets to ""

Arrays Need contentEquals()

Kotlin arrays (IntArray, Array<T>) don't override equals() for structural comparison. Two arrays with identical contents are not ==.

val a = intArrayOf(1, 2, 3) val b = intArrayOf(1, 2, 3) println(a == b) // false println(a.contentEquals(b)) // true

When you put an array inside a data class, the generated equals compares it by reference. Use List inside data classes to get structural comparison for free. This matters in graph problems where you're memoizing array states and wondering why your cache never hits.


The Stack Has a Limit. tailrec Has a Catch.

Kotlin on the JVM inherits Java's thread stack. Deep recursion throws StackOverflowError, typically around 5,000 to 15,000 frames depending on frame size. Tree problems on 10^4-node inputs can blow up mid-interview in the most demoralizing way possible.

The tailrec modifier rewrites tail-recursive calls into a loop at compile time:

tailrec fun factorial(n: Long, acc: Long = 1L): Long = if (n <= 1L) acc else factorial(n - 1, n * acc)

tailrec requires the recursive call to be the last operation in the function, and handles only a single recursive call. Tree recursion with two recursive calls doesn't qualify. Convert those to an explicit stack with ArrayDeque. Knowing this distinction under pressure is the difference between a clean pivot and staring at a StackOverflowError while the interviewer writes something down.


max() Is Deprecated. Use maxOrNull().

max() and min() were deprecated in Kotlin 1.7 because they threw NoSuchElementException on empty collections. The replacements return nullable values:

val nums = listOf(3, 1, 4) val biggest = nums.maxOrNull() // Int? -- returns null on empty val smallest = nums.minOrNull() // Int?

Add !! when you know the list is non-empty. Use ?: Int.MIN_VALUE when accumulating into a running max.

val mx = nums.maxOrNull()!! val mx2 = nums.maxOrNull() ?: Int.MIN_VALUE

LeetCode's Kotlin environment varies by version. The deprecated max() may compile but with a warning. Use the OrNull variants so you're not debugging a NoSuchElementException on a list you forgot could be empty at that point in the algorithm.


String Concatenation in a Loop Is O(n²)

Strings are immutable. Every + allocates a new string and copies everything accumulated so far. It's one of those bugs that looks fine on small inputs, passes half the test cases, then times out on the one with a 10^4-character string.

// Wrong: O(n²) var result = "" for (c in chars) result += c // Right: O(n) val sb = StringBuilder() for (c in chars) sb.append(c) val result = sb.toString()

For building strings from collections, joinToString is idiomatic and avoids the issue entirely.

val result = chars.joinToString("")

Kotlin Coding Interview Quick Reference

Frequency map:

val freq = mutableMapOf<Char, Int>() for (c in s) freq[c] = (freq[c] ?: 0) + 1 // or val freq = s.groupingBy { it }.eachCount()

Sort by multiple keys:

list.sortWith(compareBy({ it.first }, { it.second }))

BFS queue:

val queue = ArrayDeque<Int>() queue.addLast(start) while (queue.isNotEmpty()) { val node = queue.removeFirst() }

Default value in map:

map.getOrDefault(key, 0) map.getOrPut(key) { mutableListOf() }.add(value)

Knowing Them at Your Desk Is Not the Same as Catching Them Live

These bugs don't announce themselves. No compiler errors, often no crashes until a specific edge case hits. Catching an Int overflow or a discarded .sorted() while narrating your approach to an interviewer is a different skill than catching it at your desk with all the time in the world.

SpaceComplexity runs voice-based DSA mock interviews with rubric-based feedback on communication, problem-solving, and code quality. You'll encounter these Kotlin traps in a setting that matches the real thing, which is the only way to actually build the reflex.


Key Takeaways

  • mutableMapOf() is a LinkedHashMap. Use HashMap or TreeMap when you need different behavior.
  • Declare accumulators as Long when values can exceed 2 billion.
  • .sort() mutates. .sorted() returns a new copy. Discarding .sorted() is a silent bug.
  • Never subtract in a comparator. Use compareBy or compareTo.
  • PriorityQueue() is a min-heap. PriorityQueue(reverseOrder()) is a max-heap.
  • == is structural on data classes. Arrays need contentEquals().
  • data class equals and copy only see primary constructor properties.
  • tailrec converts tail calls to loops. Tree recursion needs an explicit ArrayDeque stack.
  • max() and min() are deprecated. Use maxOrNull() and minOrNull().
  • String concatenation in a loop is O(n²). Use StringBuilder or joinToString.

Further Reading