# Kotlin fundamentals

Source: https://codewiki.com/kotlin/fundamentals/

> - **what**: Kotlin is statically typed. It uses `val` and `var` to say whether a reference can be reassigned and records nullability in the type.
> - **trap**: `val` doesn't make an object immutable, and type inference doesn't add implicit numeric conversions; `!!` only postpones a null problem until runtime.
> - **fix**: Start with `val`, parse and validate data at input boundaries, and express real alternatives with null checks, `?.`, `?:`, and exhaustive `when` expressions.

## What it is and why it exists

Kotlin's basic syntax has a simple goal: put program constraints directly in the code. `val` or `var` says whether a name can be reassigned, a trailing `?` says whether a value may be `null`, and a branch can produce a result directly. The compiler can then reject many impossible states before the program runs.

Kotlin is statically typed. Every expression has a compile-time type, but local declarations usually don't repeat it because the compiler performs type inference. Inference doesn't make the language dynamic: once `val count = 3` is inferred as `Int`, a string can't later be assigned to `count`.

These rules appear in every Kotlin program, whether it targets the JVM, JavaScript, Native, or Wasm. This page uses ordinary Kotlin/JVM command-line programs to explain the language core. Android lifecycles, coroutines, collection pipelines, and class design belong to their own topics.

The syntax makes ownership and failure paths easier to read; saving a few characters is incidental. `String?` tells a caller that absence is part of the contract, while an enum `when` without `else` tells a maintainer that a new member will trigger a compile-time check.

## How it works

### Declarations bind names

`val` declares a read-only reference: after initialization, that name can't point at another value. `var` declares a reassignable reference. A local `val` may be declared first and assigned once later, but it then needs an explicit type, and the compiler must prove that every path reaches initialization before use.

A read-only reference isn't the same as an immutable object. `val quantities = intArrayOf(1, 2)` rejects `quantities = intArrayOf(3)` but permits `quantities[0] = 3`, because the array object is still mutable. `val` constrains the name; the object's type and API determine whether its state can change.

`var` fits a loop counter or the current position of a state machine: bindings that genuinely change. Declaring every local as `var` widens the mutable area and makes readers track more assignment sites. A useful test is whether the code still works after removing reassignment; if it does, the binding should be a `val`.

### Inference still produces a definite type

With an initializer, the compiler can usually infer a local variable's type. Integer literals normally become `Int` and become `Long` when they exceed the `Int` range. Literals with a decimal point default to `Double`; an `f` or `F` suffix denotes `Float`. Write the type explicitly when it carries an API contract, permits `null`, or saves the reader from guessing.

Kotlin's numeric types don't convert into one another automatically for assignment or function arguments. To pass an `Int` to an API that accepts `Long`, write `count.toLong()`. An explicit conversion keeps truncation, overflow, and precision changes visible in review.

Mixed numeric expressions can produce a wider result through their operator overloads, but that doesn't mean every context permits implicit conversion. Integer division deserves particular care: `5 / 2` is `2`. At least one operand must be floating point to get `2.5`.

### Nullability is part of the type

`String` doesn't accept `null`; the nullable type `String?` does. You can't access an ordinary member directly on a nullable receiver. Code must first prove it is non-null, use the safe-call operator `?.`, or provide an alternative through the Elvis operator `?:`.

A safe call returns `null` when its receiver is `null`, so the result of a chain often remains nullable. If any step in `profile?.address?.city` is missing, the whole expression produces `null`. `?:` evaluates its right side only when the left side is `null`, so that right side can be a fallback, `return`, or `throw`.

The compiler performs a smart cast when it can prove that a value has a more specific type along one control-flow path. After `value == null` followed immediately by `return`, the remaining path can use `value` as non-null. This proof depends on stability; an open or mutable property that other code may change usually can't be smart-cast directly.

### Branches can produce values

`if` and `when` control execution and can also return results as expressions. An `if` used as a value must cover both branches, and the final expression in each block is that branch's result. This lets you initialize a `val` directly instead of declaring a mutable temporary and assigning it in every branch.

`when` can match values, ranges, or types, and a subjectless `when` checks Boolean conditions in order. The compiler requires a `when` used as an expression to be an exhaustive `when`. Listing every enum or sealed-hierarchy case without a broad `else` turns a new member into a compile error instead of silently routing it to a default behavior.

Branches are checked in source order, and the first match wins. When ranges overlap, their order is part of the business rule. Put narrow, specific conditions before broad ones so readers don't have to derive the overlap themselves.

### Loops process sequential work

A `for` loop traverses a value that supplies an iterator, commonly a range, array, or collection. `1..5` includes both endpoints, while `1 until 5` excludes the end. Use `downTo` for descending progressions and `step` for an interval. Those boundary differences change the iteration count, and generated code often confuses `..` with `until`.

`while` checks its condition before each iteration; `do-while` runs at least once. `break` exits the nearest loop and `continue` advances to the next iteration. Labeled jumps can cross nested loops, but they make control flow more expensive to follow. If extracting a function or rewriting a condition removes the label, the result is usually easier to test.

### Functions publish contracts

Function parameters always declare types. Block-bodied functions normally declare a return type, especially in public APIs; expression-bodied functions may infer it from the expression. A function with no meaningful result returns `Unit`, and that return type can usually be omitted.

Default parameters define common behavior at the declaration site, while named arguments show a caller's choices. Named arguments also allow argument reordering, but names belong to a Kotlin API's source contract. Kotlin callers can't rely on Java parameter names to use named arguments with Java methods.

Use `require` for preconditions. A failed condition throws `IllegalArgumentException` and places the error on invalid data supplied by the caller. For expected user-input failures, a nullable value or domain result type is often better than an exception. The boundary's calling contract should decide.

## Examples

### Start with `val` and keep mutation narrow

The first program retains one mutable counter, while the customer name, array reference, unit price, and result are read-only bindings. The array's contents can still change, which demonstrates that `val` constrains a reference without freezing its object.

<!-- quick -->

```kotlin
// file: references_and_values.kt
fun main() {
    val customer = "Ari"
    val quantities = intArrayOf(2, 1, 3)

    // val blocks reference reassignment, not changes to array elements.
    quantities[1] = 2

    // Only the accumulator needs reassignment.
    var itemCount = 0
    for (quantity in quantities) {
        itemCount += quantity
    }

    val unitPriceCents = 1_250
    val subtotalCents = unitPriceCents * itemCount

    println("Customer: $customer")
    println("Quantities: ${quantities.joinToString()}")
    println("Items: $itemCount")
    println("Subtotal cents: $subtotalCents")
}
```

```text
Customer: Ari
Quantities: 2, 2, 3
Items: 7
Subtotal cents: 8750
```

<!-- /quick -->

The amount uses integer cents so this basic example doesn't introduce binary floating-point rounding. `1_250` is still an ordinary `Int`; underscores only make a numeric literal easier to read. If the product of price and quantity may exceed the `Int` range, choose `Long` or a dedicated money type before doing the arithmetic.

### Handle `null` and bad formats at the input boundary

The second program parses a raw string into `Int?`. Whitespace, an absent value, and nonnumeric text all produce `null`; the caller then separates “no usable weight” from the shipping bands for valid weights.

```kotlin
// file: nulls_and_branches.kt
fun parseWeight(raw: String?): Int? {
    val normalized = raw?.trim()

    // After the early return, normalized is smart-cast to String.
    if (normalized.isNullOrEmpty()) return null
    return normalized.toIntOrNull()
}

fun shippingBand(weight: Int): String = when {
    weight <= 0 -> "invalid"
    weight <= 10 -> "standard"
    weight <= 30 -> "heavy"
    else -> "freight"
}

fun main() {
    val inputs = listOf(" 8 ", null, "heavy", "42")

    for (raw in inputs) {
        val weight = parseWeight(raw)
        val band = if (weight == null) "missing" else shippingBand(weight)
        println("${raw ?: "<null>"} -> $band")
    }
}
```

```text
 8  -> standard
<null> -> missing
heavy -> missing
42 -> freight
```

`toIntOrNull()` keeps parsing failure in the type instead of relying on exception handling. Both sides of the `if` produce `String`, so `band` is inferred as `String`. If the business needs to distinguish “absent” from “bad format,” don't collapse both into the same `null`; return a type that preserves the separate failure reasons.

### Express common calls with default arguments

The third program fixes the unit as cents and uses `Long` for amounts. Default arguments preserve the common one-item, no-discount call. The bulk order uses named arguments to expose every changed choice.

```kotlin
// file: functions_and_arguments.kt
fun lineTotal(
    priceCents: Long,
    quantity: Int = 1,
    discountPercent: Int = 0,
): Long {
    require(priceCents >= 0)
    require(quantity >= 0)
    require(discountPercent in 0..100)

    val gross = priceCents * quantity
    return gross * (100 - discountPercent) / 100
}

fun main() {
    val regular = lineTotal(priceCents = 2_500)
    val bulk = lineTotal(
        priceCents = 2_500,
        quantity = 4,
        discountPercent = 10,
    )

    println("Regular cents: $regular")
    println("Bulk cents: $bulk")
}
```

```text
Regular cents: 2500
Bulk cents: 9000
```

The division here is integer division, so it discards any fraction smaller than one cent after the discount. That isn't a universally correct financial rounding rule; it is one concrete choice in this function's contract. Production code should name and test its rounding policy instead of letting operator order decide it silently.

### Keep enum branches exhaustive

The final program first converts an untrusted string to `Access?`, then handles the finite state exhaustively. The parser needs `else` to catch arbitrary text. The routing function lists `null` and every enum member without an `else`.

```kotlin
// file: exhaustive_when.kt
enum class Access {
    GUEST,
    MEMBER,
    ADMIN,
}

fun parseAccess(raw: String): Access? = when (raw.lowercase()) {
    "guest" -> Access.GUEST
    "member" -> Access.MEMBER
    "admin" -> Access.ADMIN
    else -> null
}

fun homeRoute(access: Access?): String = when (access) {
    null -> "/sign-in"
    Access.GUEST -> "/welcome"
    Access.MEMBER -> "/account"
    Access.ADMIN -> "/admin"
}

fun main() {
    for (raw in listOf("member", "ADMIN", "unknown")) {
        val access = parseAccess(raw)
        println("$raw -> ${homeRoute(access)}")
    }
}
```

```text
member -> /account
ADMIN -> /admin
unknown -> /sign-in
```

If `SUPPORT` is added later, `homeRoute` stops compiling and forces a maintainer to choose the new role's page. `parseAccess` accepts an open-ended input set, so it still needs a default branch. The two `when` expressions look similar but have different boundary contracts.

## Pitfalls

### Treating `val` as deep immutability

> **Pitfall:** `val` only prevents reassignment of the name. An array, mutable collection, or object with mutable properties can still change through the same reference.

**Fix:** Inspect both the binding and the object's API. Make a copy when you need a snapshot and return a read-only interface when callers shouldn't mutate through that path. If callers must be able to trust that data never changes, express immutability in the type design instead of only changing the variable to `val`.

### Relying on implicit numeric widening

> **Pitfall:** Java accepts `long total = count` through widening, but Kotlin rejects `val total: Long = count`. Simply changing the result to `Int` may reduce its representable range.

**Fix:** Call `toLong()`, `toDouble()`, or another conversion explicitly at the semantic boundary, and choose the result type before arithmetic starts. Converting to a narrower integer may change the value, while converting floating point to an integer discards the fractional part, so conversions need boundary tests too.

### Using `!!` to remove a compiler error

> **Pitfall:** `value!!` doesn't prove that a value is non-null. It asks the runtime to throw `NullPointerException` when the value is `null` and discards the business meaning of absence.

**Fix:** Pick a policy at the input boundary: use `?: return` to abandon the operation, `?: throw IllegalArgumentException(...)` to report a broken contract, or preserve `null` in the return type. Reserve a local, explained assertion for the rare case where an external invariant is already established but the compiler can't express it.

### Comparing content with `===`

> **Pitfall:** `==` checks structural equality and handles `null` safely; `===` asks whether two references point at the same object. String interning or JVM boxing caches can make an incorrect `===` happen to pass for some test values.

**Fix:** Use `==` for value comparison and `===` only when object identity is genuinely part of the algorithm. Arrays are an extra boundary: array `==` doesn't compare elements, so choose `contentEquals()` or `contentDeepEquals()` according to the data shape.

### Adding a broad `else` to finite states

> **Pitfall:** Adding `else` to a `when` over an enum or sealed hierarchy makes the code shorter, but it keeps compiling after a new state appears and may send that state down a fallback that was never designed for it.

**Fix:** List every case of a closed state so the compiler checks exhaustiveness. Keep `else` only when the input set is actually open, such as arbitrary strings, integers, or external protocol values, and define whether an unknown input is rejected, ignored, or downgraded.

### Hiding side effects inside dense expressions

> **Pitfall:** `if`, `when`, Elvis, and scope functions can all nest into one expression. Generated code sometimes combines logging, state changes, early returns, and result computation until execution order becomes hard to review.

**Fix:** Give each expression one result that is easy to name. Validate input first, calculate the value next, and perform side effects last. When a branch needs several steps, expand it with a block and local names instead of optimizing for the fewest lines.

<!-- deep -->

## Context sets the boundary of inference

Type inference uses the initializer and the expected type around it. `val count = 1` becomes `Int`, while the same literal in `val count: Long = 1` is constrained by the declared type. During resolution, integer literals have a special literal type that the compiler maps to a concrete numeric type allowed by the context and range.

Inference finds a common type that the current expression can prove, not the type future code might need. If two branches return different concrete classes, their result may be inferred as a shared parent type; introducing `null` usually makes the result nullable. An explicit return type on a public function stops an implementation refactor from accidentally changing the contract callers see.

Generic calls gather constraints from arguments and from the expected return type. A seemingly unrelated explicit type can therefore change type-parameter inference, so don't repair a type error by adding arbitrary casts. Decide which type the API should expose, then supply a clear expected type through the declaration or function signature.

Smart casts are also inference driven by control flow. The compiler records type checks, null checks, early returns, and relationships between Boolean conditions. If some other code might change the value after the check, the proof no longer holds. Reading a property into a local `val` often freezes the observed value and restores smart casting at the same time.

## `Unit` and `Nothing` describe control flow

`Unit` means a function completes normally but has no meaningful result for its caller. It is a real type with one value, `Unit`, unlike the complete absence of a value represented by `void` in some languages. A block-bodied function with no other returned value is normally inferred to return `Unit` when the annotation is omitted.

`Nothing` means an expression never produces a normal result. `throw` has type `Nothing`, and a function that never returns can declare `Nothing`. Because `Nothing` fits other result types, the full expression in `val name = input ?: throw IllegalArgumentException(...)` can still have a non-null string type.

`return` is an expression too, so it can sit on the right side of Elvis. `val id = parseId(raw) ?: return` moves the failure path out of the current function, leaving a non-null `id` in the remaining code. This guard works for one failure action; if distinct failures need distinct responses, an explicit result type keeps more information than a chain of early returns.

These types explain how branches find a common type. When one branch produces a domain value and another throws, the throwing branch doesn't force the expression to degrade to `Any`. The compiler knows that every path which continues normally carries the domain value.

## JVM representation is not the source type system

On the JVM target, non-null numbers can usually use JVM primitive representations; nullable numbers and generic positions usually need boxed objects. That implementation detail doesn't change the source-level rules for Kotlin's `Int`, but it affects signatures and nullability at Java interoperability boundaries.

Don't infer numeric equality from referential equality. The JVM may cache some boxed number objects, making equal small values happen to share a reference while equal larger values occupy separate objects. Use `==` for numeric meaning. Check identity only when identity itself is a domain concept.

Java declarations often lack the nullability information Kotlin expects. The compiler presents such boundaries as platform types, letting a caller receive the value as nullable or non-null, but a wrong choice can still cause a runtime null-pointer failure. A platform type isn't an ordinary annotation you can write in Kotlin source; it represents incomplete information at the interoperability boundary.

Narrow a Java return value to an explicit Kotlin type early. `val name: String? = javaApi.name()` preserves possible absence; when a contract guarantees non-null, validate that at the boundary and report a meaningful failure. Letting platform types travel through business code leaves the same unstated assumption behind every member access.

## Equality, arrays, and mutable content

Kotlin translates `==` into a null-safe `equals` call. `===` never calls `equals`; it only checks reference identity. Whether a class has useful structural equality depends on its `equals` implementation, and an ordinary class's default implementation may still amount to identity comparison.

Arrays don't define `==` as an element-by-element comparison. Two separate array objects holding the same sequence normally can't be compared by content with `==`. Use `contentEquals()` for a one-dimensional array and `contentDeepEquals()` for nested arrays when appropriate. A test with the wrong comparison can reject correct output or fail to inspect elements at all.

A collection's read-only interface isn't synonymous with an immutable collection either. A read-only reference may point to an object still held through a mutable alias elsewhere, so later reads can observe changes. Copy the data when an API needs a stable snapshot, and make ownership of the underlying mutable state explicit.

Ask one direct question during review: does the code constrain a name, an interface, a value, or an object's identity? Mixing the four can survive small examples. The bug appears once data is shared, crosses a Java boundary, or changes state.

## Scope, packages, and entry points

Kotlin permits functions, properties, and types at the top level of a file; it doesn't require every declaration to sit inside a class. Top-level declarations still belong to a package. On the JVM, the compiler generates a class representation to hold them, but Kotlin callers normally don't need that generated name.

A `package` declaration belongs at the top of a file, and an `import` affects name resolution only in that source file. Source directories don't have to mirror packages mechanically, though builds usually organize them that way for navigation and access review. When imports conflict, `as` supplies a local alias without scattering fully qualified names across every call.

Braces establish local scopes. An inner declaration may shadow an outer declaration with the same name, but logs and debuggers then make those values hard to distinguish. Generated code reuses names such as `result`, `value`, and `data` aggressively; inside nested blocks, replace them with names that state the unit and state.

Local variables must be initialized before they are read. When a `val` is assigned inside conditional branches, the compiler checks every path that can continue. Missing one path rejects a later read. Initializing directly from an `if` or `when` expression usually makes the “every branch supplies a value” requirement more obvious.

An ordinary command-line application uses a top-level `main` function as its entry point. It may take no arguments or receive `Array`. Business logic doesn't need to live in `main`; the entry point should turn external strings into explicit types and then call functions that can be tested independently.

File boundaries don't create access control automatically. A top-level declaration without a modifier is `public` by default. Mark a helper used only in its file as `private`, or use `internal` for module-wide access that shouldn't be public outside the module. Visibility is an API choice, not cleanup to postpone until library publication.

Review a new basic Kotlin file by checking its boundaries in this order:

1. Do the package and imports make dependency origins clear?
2. Do top-level declarations use the narrowest reasonable visibility?
3. Does the entry point parse and validate at the boundary?
4. Do business functions receive only values with explicit types and units?

This order separates syntax concerns from architecture concerns. The compiler proves names, types, and initialization. The author still decides which values are trusted, which declarations are public contracts, and how to report a failed operation to the caller.

<!-- /deep -->

[Checkpoint: kotlin/fundamentals](https://codewiki.com/kotlin/fundamentals/#checkpoint)

## Further reading

- [Kotlin basic syntax overview (official documentation source)](https://raw.githubusercontent.com/JetBrains/kotlin-web-site/master/docs/topics/basic-syntax.md)
- [Kotlin number types (official documentation source)](https://raw.githubusercontent.com/JetBrains/kotlin-web-site/master/docs/topics/numbers.md)
- [Kotlin null safety (official documentation source)](https://raw.githubusercontent.com/JetBrains/kotlin-web-site/master/docs/topics/null-safety.md)
- [Kotlin conditions and loops (official documentation source)](https://raw.githubusercontent.com/JetBrains/kotlin-web-site/master/docs/topics/control-flow.md)
- [Kotlin functions (official documentation source)](https://raw.githubusercontent.com/JetBrains/kotlin-web-site/master/docs/topics/functions.md)
