skip to content

How are template expressions evaluated and converted to text? Walk through toString() invocation, null, and custom types.

level: middleimportance: should knowfreq 50%

answer

  1. template = evaluate then toString()
  2. null -> literal "null", no NPE
  3. data class gives readable toString()
  4. plain class gives Class@hash
  5. arrays print [I@hash — use joinToString

basics

~10 s

Kotlin runs the expression, then calls its toString() to get text. A null becomes the word "null". For your own classes, the result depends on the toString() you write (or the default one).

solid answer

~40 s

Each template expression is evaluated at runtime, and the result is coerced to `String` by calling `toString()` on it. For nullable values, Kotlin emits the literal `"null"` rather than throwing — it effectively does a null-safe `toString()`. For a custom class, the output is whatever that class's `toString()` returns: by default that's `ClassName@hashHex` (e.g. `User@1b6d3586`), but a `data class` auto-generates a readable `toString()` like `User(name=Ada, age=30)`. Numbers use their `toString()` formatting, which can be locale-independent for primitives but matters for some types. Because templates always go through `toString()`, overriding `toString()` is the cleanest way to control how an object appears in logs and interpolated messages.

code

kotlin · 6 lines
kotlin
data class User(val name: String, val age: Int)
val u: User? = null
println("$u")                 // null
println("${User("Ada", 30)}") // User(name=Ada, age=30)
println("${intArrayOf(1,2,3)}")          // [I@... (trap)
println("${intArrayOf(1,2,3).joinToString()}") // 1, 2, 3

go deeper

for a junior

Knows the value is turned into text and null shows as "null".

for a middle

Explains toString() invocation, data-class vs plain-class output, and null safety.

for a senior

Calls out the array toString() trap and locale/formatting concerns, recommending overridden toString() or explicit formatting.

for a principal

Sets conventions for domain toString() (no secrets/PII in logs) and distinguishes debug rendering from user-facing formatting.

## Evaluation order For `"a ${expr} b"`, Kotlin: 1. Emits the literal text `"a "`. 2. Evaluates `expr` (a full runtime expression — side effects included). 3. Converts the result to `String` via **`toString()`**. 4. Appends `" b"`. The compiler lowers all of this to `StringBuilder.append(...)` (or plain concatenation), so it's ordinary code, not reflection. ## null handling If the expression's static type is nullable and the value is `null`, the template emits the literal text `"null"`. It does **not** throw an NPE — the lowering uses a null-safe conversion equivalent to `String.valueOf`. So `"x=${maybeNull}"` yields `"x=null"` when the value is null. ```kotlin val s: String? = null println("value=$s") // value=null ``` ## Custom types and toString() The rendered text is **entirely determined by `toString()`**: ```kotlin class Plain(val id: Int) data class Point(val x: Int, val y: Int) println("${Plain(7)}") // Plain@<hash> (default Object-style toString) println("${Point(1, 2)}")// Point(x=1, y=2) (data class auto toString) ``` - A normal class inherits `Any.toString()`, producing `FQN@hexHashCode`. - A **`data class`** auto-generates a `toString()` listing its component properties. - Overriding `toString()` yourself gives full control: ```kotlin class Money(val cents: Int) { override fun toString() = "$%.2f".format(cents / 100.0) } println("Price: ${Money(1299)}") // Price: $12.99 ``` ## Collections and arrays Kotlin collections (`List`, `Set`, `Map`) have meaningful `toString()` (`[1, 2, 3]`, `{a=1}`). **Java arrays are the trap**: `IntArray`/`Array<T>` inherit the default object `toString()`, so `"${intArrayOf(1,2,3)}"` prints something like `[I@1b6d3586`. Use `.joinToString()` or `.contentToString()` instead. ## Practical guidance - Override `toString()` on domain types you log/interpolate. - Be aware that number `toString()` may differ from locale-aware formatting; for user-facing currency/percent formatting use `String.format`/`NumberFormat`, not raw interpolation. - For arrays, always convert explicitly.

  • Why does interpolating an IntArray print garbage like [I@1b6d3586?
    Java arrays don't override toString(); they use Object's default. Use joinToString() or contentToString() for readable output.
  • How do you make your own class render nicely in templates?
    Override toString() (or make it a data class), since templates always call toString() on the value.

saying these in an interview costs you the question

  • Thinks null in a template throws an NPE
  • Expects a plain class to print its fields without overriding toString()
  • Doesn't know arrays print as [I@hash
  • Believes interpolation does locale-aware number formatting automatically
  • Says templates use the class name via reflection rather than toString()

context