Skip to content

L2: Variables, Types, and the Truth about Null Safety

In 1965, British computer scientist Sir Tony Hoare introduced the concept of the null reference (null) while designing the ALGOL W language. Decades later, he famously apologized in a public conference, calling it his “billion-dollar mistake” because it has sparked countless system outages, security vulnerabilities, and debugging marathons across languages like Java, C, and C++.

The infamous NullPointerException (NPE) happens when your code attempts to interact with an object you assumed was present, but in reality is completely absent (null). Imagine trying to start a car, but the engine literally does not exist inside the engine bay.

Kotlin was deliberately architected from day one to eliminate this problem at compile time.


In Kotlin, you can think of variables as labeled boxes where you store data. There are two primary varieties of boxes:

A standard open container. You can deposit a piece of data, retrieve it, and place a different piece of data inside at any time. Its contents can freely mutate or vary.

// 'var' stands for Variable. By using var, we notify the Kotlin compiler
// that this container is explicitly designed to be reopened and reassigned.
var age = 25
// Because age is declared with 'var', mutating its reference is fully valid.
age = 26

A container where you place a piece of data and seal it with tape. Once you assign a value, you cannot reassign that identifier to point to another object. This is known as a Read-Only reference.

// 'val' stands for Value. Using val establishes a Read-Only binding.
// We instruct Kotlin: "Once data is placed here, seal the box."
// This makes application state predictable and safe.
val username = "jesusdmedinac"
// ❌ COMPILE ERROR: Val cannot be reassigned.
// Kotlin prevents this mutation because it violates the 'val' contract.
username = "guest"

🧠 Conceptual Bridge: Variables and Immutability in Your Language

Section titled “🧠 Conceptual Bridge: Variables and Immutability in Your Language”
  • val in Kotlin resembles const in JavaScript/TypeScript: the reference cannot be reassigned.
  • However, in TypeScript const arr = ["A", "B"] allows arr.push("C") without warning. In Kotlin, we explicitly distinguish between read-only interfaces (List, created with listOf) and mutable interfaces (MutableList, created with mutableListOf).
  • var corresponds directly to let in modern JS/TS.

Kotlin’s type system enforces a strict architectural boundary between references that can hold null (boxes that might be empty) and references that guarantee a non-null value (boxes guaranteed to contain data).

To instruct Kotlin that a container might hold null, we append a question mark ? to the type:

// By explicitly declaring the type as 'String' (without ?),
// we promise Kotlin that this box will NEVER be empty.
var name: String = "Jesus"
// ❌ COMPILE ERROR: Kotlin prevents null assignment,
// protecting the non-nullable contract.
name = null
// By appending '?', we create a Nullable Type (String?).
// We declare: "I am aware that this container might be empty in the future."
var nullableName: String? = "Daniel"
// ✅ Permitted. Because the type is String?, Kotlin allows emptying the box.
nullableName = null

Ways to Inspect the Box (Safe Null Handling)

Section titled “Ways to Inspect the Box (Safe Null Handling)”

Kotlin provides a comprehensive set of language operators to inspect and handle nullable types safely.

The most familiar approach for Java developers is a standard conditional check:

if (nullableName != null) {
// Inside this branch, Kotlin automatically smart-casts nullableName
// into a safe, non-nullable String because non-nullness is mathematically proven.
println(nullableName.length)
}

Executes the property or method call only if the target reference is non-null. If the box is empty, it short-circuits and safely returns null instead of blowing up:

// If nullableName is null, 'length' safely resolves to null. No crash!
val length = nullableName?.length

Named for looking like Elvis Presley’s iconic hairstyle tilted sideways ?:. It pairs with safe calls to provide a fallback default value whenever an expression resolves to null:

// "Use nullableName, but if it is null, fallback to 'Guest'"
val finalName = nullableName ?: "Guest"

The self-destruct button. It tells the compiler: “Trust me, I am 100% positive this is not null, and if I am mistaken, I take full responsibility for crashing the runtime.”

// 💥 WARNING: If nullableName is null at runtime,
// this line immediately throws a NullPointerException and terminates the process.
val dangerLength = nullableName!!.length

🧠 Conceptual Bridge: Null Safety in Your Language

Section titled “🧠 Conceptual Bridge: Null Safety in Your Language”
  • TypeScript provides the strictNullChecks flag in tsconfig.json. When active, string | null behaves almost identically to Kotlin’s String?.
  • Kotlin’s Safe Call (?.) and Elvis operator (?:) directly mirror JavaScript’s Optional Chaining (?.) and Nullish Coalescing (??).
  • Kotlin’s Smart Casts correspond to TypeScript Type Guards (if (typeof x === "string")).
  • Kotlin’s !! operator is the exact twin of TypeScript’s non-null assertion operator ! (obj!.prop).

Behind the Scenes: Platform Types (String!)

Section titled “Behind the Scenes: Platform Types (String!)”

There is an important boundary where Kotlin cannot offer compile-time null safety guarantees: Java Interoperability. Because Kotlin runs seamlessly on the JVM, you can invoke libraries written in Java directly from Kotlin without adapters.

However, Java lacks native language-level null safety. When Kotlin receives a return value from Java that lacks @Nullable or @NotNull annotations, the compiler cannot determine whether the object is safe (String) or nullable (String?).

To handle this smoothly without forcing verbose syntax everywhere, Kotlin introduces Platform Types (rendered visually in your IDE with an exclamation mark, like String!).


Today we evolve our AI Chat CLI. Instead of just printing a static welcome banner, we will ask the user for their name and greet them dynamically.

What happens if the user presses Ctrl+D (End-of-File / EOF) and submits nothing? The standard input stream returns null. This is where our null safety foundations shine.

In Lesson 1 we established our main entry point and welcome banner. Keep that banner in place; we are building upon it!

Directly below the println lines of your welcome banner, prompt the user for their name. Use print (without the ln suffix) so the terminal cursor awaits input on the same line:

print("Please enter your name: ")

Read what the user types using readlnOrNull(). Add this line below your prompt:

// readlnOrNull() returns a nullable String (String?) because terminal
// reading may be interrupted or terminated via EOF (Ctrl+D).
val input: String? = readlnOrNull()

Because input is typed as String?, Kotlin will not permit unsafe dereferencing. Let’s use an idiomatic if/else block to validate that the input is non-null and not empty whitespace:

// Declare 'username' as a val (Read-Only) since it remains fixed in this flow.
val username: String
// Verify that input is not null AND not merely blank whitespace
if (input != null && input.isNotBlank()) {
username = input // Smart Cast in action! 'input' is guaranteed to be a non-null String here.
} else {
username = "Guest" // Safe default fallback
}

Finally, add a personalized greeting using Kotlin String Templates (evaluating identifiers with $):

println("\nHello, $username! How can I assist you today?")

Step 5: Run and Verify in Your Environment

Section titled “Step 5: Run and Verify in Your Environment”

Launch your application using your preferred workflow:

  • Option A (IntelliJ IDEA / Android Studio):

    1. Click the green Play (▶️) button next to fun main().
    2. In the bottom Run window, type your name when prompted and press Enter.
  • Option B (Google Antigravity / Visual Studio Code):

    1. Press Cmd + Shift + B (Mac) or Ctrl + Shift + B (Windows/Linux). Alternatively, open the command palette (Cmd + Shift + P), type Tasks: Run Task, and select Run AI Chat CLI.
    2. The terminal session will open interactively awaiting your input.
  • Option C (Command-Line Terminal):

    1. Open your terminal in the repository root and run:
      Ventana de terminal
      ./gradlew run --console=plain -q
      (On Windows CMD/PowerShell: gradlew.bat run --console=plain -q)
    2. The --console=plain -q flags suppress Gradle daemon progress bars for clean terminal interaction.

Verifying Null Safety: Run the program a second time, but press Enter immediately without typing anything (or input only spaces). Notice how the validation logic safely falls back to "Guest", preventing any risk of a NullPointerException.

Congratulations! You have implemented robust I/O handling and defended your application against runtime crashes.



Have questions about mutability (val/var), Platform Types, or the Elvis operator?