10 min read

Kotlin Language: Variables & Types Refresher

A spaced-repetition refresher on Variables & Types in Kotlin Language, focused on practical implementation details and updates.

#Learning-log#Kotlin Refresher

Table of contents

  1. Context and Scope
  2. Conceptual Model & Analogy
  3. Deep Dive
  4. Implementation Patterns
  5. Baseline Example
  6. Production-grade/Advanced Example (combining several concepts)
  7. Common Pitfalls and Tradeoffs
  8. Technical Note
  9. Sources & Further Reading
  10. Check Your Work

Context and Scope

Run date: 2026-03-05. This is a focused refresher on Kotlin variables and core types for the “Kotlin Language → Variables & Types” track. We’ll cover exactly these 11 concepts: val, var, Type Inference, String, String Templates, Int/Long/Double/Float, Boolean, Char, Any, Unit, and Nothing. The prior post was kotlin-variables-types-new-topic-2026-03-04-2, last reviewed 2026-03-04.

Conceptual Model & Analogy

Think of memory like labeled containers on a workbench:

  • val is a sealed, labeled container: you fill it once and can’t swap it out for another. You can still open what’s inside and change its contents if that object itself is mutable.
  • var is a resealable container: you can replace what’s inside with something else of the same type.
  • Types are the shape of containers: they determine what fits.
  • Any is the ultimate “catch-all” container shape that anything can fit into.
  • Unit is a label for “this operation doesn’t produce a meaningful thing.”
  • Nothing is “there is no possible thing here,” used when a function never returns.

Deep Dive

  1. val (read-only variables)
  • You assign a val exactly once. Reassignment is prohibited, but the referenced object may still mutate (for example, a MutableList). Official docs recommend defaulting to val. (kotlinlang.org)
  1. var (mutable variables)
  • You can reassign a var any number of times after initialization. Prefer val by default and use var when mutation is essential. (kotlinlang.org)
  1. Type Inference (local and expression-level)
  • Kotlin infers most local variable and single-expression function return types from initializers/expressions, so you can omit explicit annotations where the intent is clear. With the K2 compiler (default since Kotlin 2.0), inference and smart casts improved, reducing the need for workarounds in complex scenarios. For public API surfaces, keep explicit types for clarity and binary compatibility. (kotlinlang.org)
  1. String
  • Kotlin’s String is immutable. You can create escaped strings (”…”) or raw, triple-quoted multiline strings ("""…"""). Raw strings preserve newlines and don’t process backslash escapes; use trimMargin/trimIndent to clean leading whitespace. (kotlinlang.org)
  1. String Templates
  • Interpolate values with $name or ${expression}. In multiline strings you can’t backslash-escape characters; to insert a literal $ use ”${’$’}”. As of Kotlin 2.1 (Preview), multi-dollar string interpolation lets you choose how many consecutive $ characters trigger interpolation, dramatically simplifying strings that contain many literal dollar signs (enable with -Xmulti-dollar-interpolation). (kotlinlang.org)
  1. Int / Long / Double / Float
  • Kotlin provides Int and Long for integral values; Double and Float for floating-point per IEEE 754. On the JVM, non-null numeric values are compiled to primitives for performance, and become boxed (wrapper objects) when nullable or used in generics; identity (===) on boxed numbers can surprise due to JVM caching of small values—use == for numeric equality. Kotlin does not perform implicit widening conversions; call toX() explicitly. (kotlinlang.org)
  1. Boolean
  • Boolean holds true or false. Operators || and && short-circuit; ! negates. On the JVM, Boolean is a primitive when non-null and boxed when nullable. (kotlinlang.org)
  1. Char
  • Char represents a single Unicode character literal in single quotes, distinct from a one-character String. Use digitToInt/digitToIntOrNull for numeric conversions; on the JVM, Char is a primitive when non-null and boxed when nullable. (kotlinlang.org)
  1. Any
  • Any is the root supertype of all non-null Kotlin types (Any? is the top of the entire hierarchy, including null). It defines equals, hashCode, and toString. On the JVM, Any maps to Object at runtime. Prefer specific types in APIs; use Any sparingly for heterogeneous values. (kotlinlang.org)
  1. Unit
  • The return type of functions that don’t return a meaningful value. The compiler infers Unit for block-bodied functions without an explicit return value; you usually omit it except where a function type requires it, e.g., () -> Unit. (kotlinlang.org)
  1. Nothing
  • A bottom type with no values; a function returning Nothing never returns normally (it always throws or loops forever). This helps the compiler reason about control flow (code after a Nothing call is unreachable). The stdlib exposes Nothing as a real type across Kotlin targets. (kotlinlang.org)

Implementation Patterns

  • Default to val; introduce var only when mutation is essential and localized. This makes data flow easier to reason about and reduces bugs. (kotlinlang.org)
  • Prefer type inference for locals; specify explicit types for public APIs and overload-heavy code to improve readability, refactor safety, and incremental builds. (kotlinlang.org)
  • Prefer String templates over concatenation; for content heavy in literal $ (templating, JSON schema, shell), enable multi-dollar interpolation and use $$… or $$$… raw strings. (kotlinlang.org)
  • Choose numeric types deliberately:
    • Int unless values exceed Int range; then Long.
    • Double for general floating-point; Float for memory-constrained or interop-specific cases.
    • Use underscores in numeric literals for readability (1_000_000). Avoid ==/=== confusion on boxed numbers. (kotlinlang.org)
  • Distinguish Char vs String; prefer Char for single code units and String for text. Use Char APIs for code-point/digit work. (kotlinlang.org)
  • Any is for generic holders or dynamic data ingress; narrow as soon as possible (is checks + smart casts). (kotlinlang.org)
  • Model void-like operations with Unit and “never returns” paths with Nothing (e.g., fail()). This improves exhaustiveness and static reasoning. (kotlinlang.org)

Baseline Example

fun main() {
    // val vs var
    val pi = 3.14159          // inferred Double
    var count = 0             // inferred Int
    count += 1                // mutable

    // Strings
    val name = "Kotlin"
    println("Hello, $name!")  // template

    // Raw multiline string (no escaping)
    val banner = """
        |Welcome to $name
        |Version: ${2}.${1}
    """.trimMargin()
    println(banner)

    // Numbers: explicit conversions, no implicit widening
    val i: Int = 42
    val d: Double = i.toDouble()  // must convert
    println("As double: $d")

    // Boolean and Char
    val isReady: Boolean = true
    val letter: Char = 'A'
    println("Ready? $isReady, first letter: $letter")
}

Production-grade/Advanced Example (combining several concepts)

// A tiny config loader that demonstrates: String templates (incl. multi-dollar),
// type inference, Any narrowing, Unit, Nothing, and numeric conversions.

// Enable multi-dollar interpolation in your build (Kotlin ≥ 2.1):
// kotlin { compilerOptions { freeCompilerArgs.add("-Xmulti-dollar-interpolation") } }

data class AppConfig(
    val port: Int,
    val debug: Boolean,
    val apiKey: String
)

// A helper that never returns: useful for “fail-fast” configuration checks.
fun fail(message: String): Nothing = throw IllegalStateException(message)

// Parse a loosely-typed map (e.g., from JSON/YAML/env) using Any and narrow types early.
fun loadConfig(raw: Map<String, Any?>): AppConfig {
    val port = when (val p = raw["PORT"]) {
        is Int -> p
        is String -> p.toIntOrNull() ?: fail("PORT must be an Int")
        null -> 8080
        else -> fail("Unsupported type for PORT: ${p::class.simpleName}")
    }

    val debug = when (val d = raw["DEBUG"]) {
        is Boolean -> d
        is String -> d.equals("true", ignoreCase = true)
        null -> false
        else -> fail("Unsupported type for DEBUG: ${d::class.simpleName}")
    }

    val apiKey = (raw["API_KEY"] as? String)?.takeIf { it.isNotBlank() }
        ?: fail("API_KEY is required and cannot be blank")

    return AppConfig(port = port, debug = debug, apiKey = apiKey)
}

// Use multi-dollar string interpolation for a JSON-like template that contains many `$` signs.
fun renderDiagnostics(cfg: AppConfig): String {
    // With `$$` prefix: two consecutive `$` trigger interpolation; single `$` stays literal.
    return $$"""
        {
          "$schema":   "https://example.org/schema",
          "$id":       "app://config",
          "debug":     ${cfg.debug},
          "http": {
            "port":   ${cfg.port},
            "format": "$$HTTP_$$METHOD"   // literal placeholders like "$HTTP_$METHOD"
          },
          "meta": "Service: $${System.getProperty("user.name") ?: "unknown"}"
        }
    """.trimIndent()
}

fun main() {
    val raw: Map<String, Any?> = mapOf( // type inference for locals; explicit in APIs
        "PORT" to "9090",
        "DEBUG" to "TRUE",
        "API_KEY" to "sk_live_123"
    )
    val cfg = loadConfig(raw)
    println(renderDiagnostics(cfg)) // returns a String; println is a Unit-returning function
}
  • Notes:
    • fail returns Nothing, so the compiler knows subsequent code is unreachable on those branches. (kotlinlang.org)
    • renderDiagnostics uses multi-dollar string interpolation ($$""" … """) to keep many $ signs literal without ${’$’} noise. (kotlinlang.org)
    • We narrow Any to concrete types using when + is checks; smart casts apply. (kotlinlang.org)

Common Pitfalls and Tradeoffs

  • “val is immutable” confusion: val prevents reassignment of the reference, not mutation of the referenced object. For true immutability, use immutable data structures or expose read-only views. (kotlinlang.org)
  • Overusing inference at API boundaries: it can obscure intent and slow incremental builds. Use explicit signatures for public APIs, but leverage inference in local scopes. (kotlinlang.org)
  • String templates vs formatting: prefer templates for readability; use String.format on Kotlin/JVM only when you need advanced locale-aware formatting. (kotlinlang.org)
  • Dollar signs in raw strings: before Kotlin 2.1 you had to write ”${’$’}”. In Kotlin 2.1+ (Preview), prefer multi-dollar interpolation for heavy-$ content, but remember to enable the compiler flag. (kotlinlang.org)
  • Numeric boxing and ===: nullable or generic numeric values are boxed; identity equality (===) can be surprising because of JVM caches for small integers. Use == for value equality. (kotlinlang.org)
  • No implicit numeric widening: explicitly convert with toX(); this avoids silent precision issues. (kotlinlang.org)
  • Char vs String: a ‘1’ is a Char, not a String; convert consciously (e.g., digitToInt). (kotlinlang.org)

Technical Note

  • Multi-dollar string interpolation is in Preview as of Kotlin 2.1 and requires opt-in with -Xmulti-dollar-interpolation (or Gradle compilerOptions freeCompilerArgs). This does not break existing single-dollar templates. If you ship libraries, document the flag for contributors. (Checked on 2026-03-05.) (kotlinlang.org)
  • K2 compiler (default since 2.0) improves type inference and smart casts; if migrating from 1.9 or earlier, consult the K2 migration guide for subtle behavior changes (for example, better common-supertype smart casts in disjunctions). (kotlinlang.org)
  • Numbers: some legacy numeric conversion functions are deprecated for certain types (for example, toByte() from Float/Double); audit older code during upgrades. (kotlinlang.org)

Sources & Further Reading

Check Your Work

Hands-on Exercise

  • Task: Implement parseArgs(args: Array): Map<String, Any?> that recognizes:
    • —port=NNN (Int), —debug=(true|false) (Boolean), —name=STRING (String), and ignores unknown flags.
    • Use val by default and introduce var only where needed.
    • Use type inference locally but give parseArgs an explicit return type.
    • Build a summary banner using a raw multiline string with either ”${’$’}” or multi-dollar interpolation if you’ve enabled it.

Brain Teaser

  • You have:
    • val a = 128; val b: Int? = a; val c: Int? = a
    • What do (b == c) and (b === c) return on the JVM, and why?
    • Now set val a2 = 100; val b2: Int? = a2; val c2: Int? = a2; What changes?
    • Explain your answers referencing boxing and small-integer caches, and state why you should use == for numeric comparisons. (kotlinlang.org)

References

Share

More to explore

Keep exploring

Previous

Jetpack Compose: Core Concepts Deep Dive (Part 2/2)

Next

Weekly Engineering Mastery Quiz (2026-03-02 to 2026-03-06)