Advanced Patterns

Kotlin Intermediate — Advanced Patterns
Kotlin Intermediate
Course 2 · Chapter 7 · Advanced Patterns

⚙️ Advanced Patterns

This chapter is a grab-bag of four smaller, self-contained tools that come up throughout real Kotlin code: value classes for zero-cost type-safe wrappers, operator overloading for expressive custom types, contracts for teaching the compiler about your own smart-cast-friendly functions, and type aliases for readability.

📦 Value Classes — Type Safety with Zero Overhead

A common bug: a function taking two Int parameters (say, createUser(id: Int, age: Int)) is easy to call with the arguments accidentally swapped — the compiler can't catch it, since both are just Int. A value class wraps a single value in a distinct type that the compiler can tell apart, while compiling away to nothing extra at runtime:

@JvmInline value class UserId(val value: Int) @JvmInline value class Age(val value: Int) fun createUser(id: UserId, age: Age) { /* ... */ } createUser(UserId(42), Age(30)) // createUser(Age(30), UserId(42)) // Compile error — wrong types, caught immediately

At runtime, a UserId is represented as a plain Int wherever possible — the wrapper is erased by the compiler in most cases, so there's no extra object allocation or memory overhead compared to using a raw Int. The type safety is entirely a compile-time guarantee, at effectively zero runtime cost.

⚠ Exactly One Property, Read-Only

A value class must wrap exactly one val property, and can't have additional state — this single-property restriction is what makes the "erase to the underlying type" optimization possible. It can still have functions and computed properties, just no extra stored state beyond the one wrapped value.

➕ Operator Overloading

Marking a function operator lets it be called using an operator symbol instead of its name — Kotlin maps specific function names (plus, minus, times, compareTo, and others) onto +, -, *, comparison operators, and more:

data class Point(val x: Int, val y: Int) { operator fun plus(other: Point) = Point(x + other.x, y + other.y) operator fun times(scale: Int) = Point(x * scale, y * scale) } val p1 = Point(1, 2) val p2 = Point(3, 4) println(p1 + p2) // Point(x=4, y=6) — calls p1.plus(p2) println(p1 * 3) // Point(x=3, y=6) — calls p1.times(3)

Common Overloadable Operators

OperatorFunction NameOperatorFunction Name
a + bplusa[i]get
a - bminusa[i] = vset
a * btimesa in bcontains
a > b / a < bcompareToa()invoke
a == bequals-aunaryMinus

This is the same underlying idea as Java's lack of true operator overloading (Java only special-cases + for String concatenation) versus, say, Python's __add__ dunder methods — Kotlin's version is Python-like in spirit, but statically type-checked, and limited to a fixed, well-known set of operator names rather than allowing arbitrary custom syntax.

⚠ Use Sparingly — Readability Matters

Operator overloading is genuinely useful for types where the operation has an obvious, unsurprising meaning (vectors, money, matrices). Overloading + on a type where it isn't intuitively "addition" (making user1 + user2 merge two accounts, say) tends to make code harder to read, not easier — the operator symbol should match what a reader would already guess it does.

📜 Contracts — Teaching the Compiler About Your Functions

Course 1's smart casts (Chapter 3) automatically narrow a nullable type after a built-in null check like if (x != null). A contract block extends that same smart-cast behavior to a custom function:

import kotlin.contracts.contract fun isNotNullOrEmpty(str: String?): Boolean { contract { returns(true) implies (str != null) } return !str.isNullOrEmpty() } fun greet(name: String?) { if (isNotNullOrEmpty(name)) { println(name.length) // smart-cast to String — the compiler trusts the contract } }

Without the contract block, name.length above wouldn't compile — the compiler has no way to know that isNotNullOrEmpty returning true guarantees name isn't null, since it's just an ordinary function as far as the compiler is concerned. The contract explicitly promises that relationship.

⚠ An Unchecked Promise — Get It Right

Contracts are not verified by the compiler — writing a contract that doesn't actually match the function's real behavior compiles fine, but produces incorrect smart-casts that can crash at runtime. This is genuinely advanced, low-level territory: most Kotlin code never writes a contract, and the standard library's own functions like isNullOrEmpty and requireNotNull are really the main examples worth knowing exist.

🏷️ Type Aliases — A Name, Not a New Type

typealias gives an existing type a new, more readable name — it's purely a compile-time label, not a distinct type the way a value class is:

typealias UserMap = Map<String, List<Int>> typealias ClickHandler = (x: Int, y: Int) -> Unit fun processUsers(users: UserMap) { /* ... */ } // far more readable than the raw Map<...> type val onClick: ClickHandler = { x, y -> println("Clicked at $x, $y") }

UserMap and Map<String, List<Int>> are completely interchangeable to the compiler — a UserMap can be passed anywhere a Map<String, List<Int>> is expected and vice versa, with no conversion. That interchangeability is exactly the difference from a value class: typealias is for readability on complex generic types, not for type safety.

value class vs typealias

value classtypealias
Distinct type?Yes — compiler tells it apart from the wrapped typeNo — fully interchangeable with the original
PurposeType safety (prevent mixing up similar values)Readability (shorten a long/complex type)
Runtime representationErased to the wrapped type where possibleN/A — never existed as a separate type

💻 Coding Challenges

Challenge 1: A Value Class for Type Safety

Define two value classes, Meters(val value: Double) and Feet(val value: Double). Write a function fun metersToFeet(m: Meters): Feet that converts (1 meter = 3.28084 feet). Demonstrate calling it correctly, and add a commented-out line showing a call that would be a compile error (e.g. passing a Feet where Meters is expected).

Goal: Practice value classes preventing a realistic unit-mixup bug.

→ Solution

Challenge 2: Operator Overloading for a Money Type

Write a data class Money(val cents: Int) with overloaded plus (adding two Money values) and compareTo (comparing by cents) operators. Demonstrate adding two Money values with + and comparing two with >.

Goal: Practice writing operator functions with a genuinely intuitive use case.

→ Solution

Challenge 3: A Type Alias for a Callback

Define a type alias ValidationResult for Pair<Boolean, String> (valid flag + message), and a type alias Validator for (String) -> ValidationResult. Write a function fun validateUsername(): Validator returning a lambda that checks a username is at least 3 characters, returning an appropriate ValidationResult. Call it and print the result.

Goal: Practice using type aliases to make a function-type signature more readable.

→ Solution

💡 Four Tools for Four Different Jobs

Reach for a value class when two things share a primitive type but must never be mixed up. Reach for operator overloading when an operation on a custom type has an obvious real-world meaning. Reach for a type alias purely to make a complex type easier to read. Contracts are the outlier — worth understanding, rarely worth writing yourself.

🎯 What's Next

Next chapter — the final chapter of Kotlin Intermediate: Kotlin Multiplatform Basics — KMP intro, the expect/actual pattern, and sharing code across platforms.