---
title: Variables
source: 'https://academia.sh/en/courses/kotlin/variables'
course: 'The Kotlin Language'
language: en
updated: '2026-10-21T05:02:40+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'States the difference between `val` and `var` and when reassignment is rejected'
    - 'Explains what type inference finds and when writing an explicit type is required'
    - 'Shows that two variables taking the same constant point to the same object, while a value built at run time falls under the content criterion'
---

# Variables

Introduces declaration with `val` and `var`, type inference, and the `const val` constant; measures that the same string constant is a single object in two variables.

In the previous lesson we saw the four criteria of equality across eight values. In this lesson we look at how a value is bound to a name: variables declared with `val` and `var`. The question is this: if I bind the same constant to two separate variables, are two separate objects created, or is a single object given two names?

## val, var, and Type Inference

In Kotlin, you declare a variable with either `val` or `var`. A `val` is a read-only variable: once the first value is given, no other value can be assigned to the same name. A `var` can be reassigned. Neither form forces you to write the type when you give a value; the compiler finds the type from the expression on the right. This inference (type inference) happens at compile time, and the type of the variable stays fixed afterward. If you want to write the type explicitly, you put a colon and the type between the name and the value.

It would be wrong to think that the object a `val` variable is bound to never changes: `val` says only that the name can never be bound to another value again. If the object it is bound to can change internally, it can change. This distinction will be useful in later lessons; here we look only at whether the name can be reassigned.

In this lesson too, the measurements are made with the helper file introduced in the first lesson. The `Criterion` class takes a value, its twin, and a different value, and asks three questions; the file is given below exactly as it was.

```kotlin
// Criterion.kt — classifies the criterion that makes two values equal
class Criterion(val label: String, first: Any?, twin: Any?, other: Any?) {
    val sameObject = first === twin
    val equalTwin = first == twin
    val equalOther = first == other

    val name: String = when {
        sameObject -> "singleton"
        !equalTwin -> "identity"
        !equalOther -> "content"
        else -> "partial-content"
    }

    fun report() {
        println("$label: same-object=$sameObject, twin=$equalTwin, other=$equalOther -> $name")
    }
}

fun summarize(checks: List<Criterion>) {
    for (name in listOf("singleton", "identity", "content", "partial-content")) {
        println("$name: ${checks.count { it.name == name }}")
    }
}
```

The program below shows the four faces of declaration together. `city` cannot be reassigned, `visits` is reassigned, `ratio` writes its type explicitly, and `inferred` takes its type from inference. The `const val` at the top of the file is a compile-time constant: a constant whose value is known during compilation, which can be only a primitive type or a string, and which stands at the top level or inside an object declaration.

```kotlin
// Main.kt — val, var, type inference, and const val
const val MAX_VISITS = 3

fun main() {
    val city = "Ankara"
    var visits = 1
    visits = visits + 1
    val ratio: Double = 2.0
    val inferred = 7
    println("$city, visits: $visits, ratio: $ratio")
    println("inferred type: ${inferred::class.simpleName}")
    println("upper limit: $MAX_VISITS")
}
```

```
Ankara, visits: 2, ratio: 2.0
inferred type: Int
upper limit: 3
```

In the output, the line `Ankara, visits: 2, ratio: 2.0` shows that the `visits` variable was reassigned once. The line `inferred type: Int` confirms that the type inferred from the constant `7` is `Int`; we asked for it at run time with `inferred::class.simpleName`. The last line prints the value of the `const val` constant. The same output was obtained in the sandbox with Kotlin 2.4.21.

**LB8.** The inferred type was measured only with the constant `7`; what inference does in contexts that require `Long` or `Double` is the subject of the next lesson.

**LB9.** `const val` was measured only at the top of the file; the rules for its form inside an `object` or a `companion object` are outside this lesson.

The practical consequence of this output is as follows. A counter such as `visits` wants `var`, because its value changes in the flow of the program. A name such as `city` wants `val`, because binding it to another value after the first assignment would be a mistake. The decision is made according to the role of the variable; whether the type is written or inferred does not affect it.

## Reassignment Is Rejected

Assigning a value a second time to a variable declared with `val` is rejected by the compiler. We saw this by compiling in the sandbox; the fragment below is therefore without output and carries no `main`.

```kotlin
val limit = 10
limit = 20
```

The compiler rejects the second line because a `val` cannot be reassigned, and the program never runs. This makes reassignment a compile-time problem and not a run-time one. In Kotlin, `val` is the default choice; `var` is written only when the value really has to be reassigned. A reader looking at a name then does not have to search for whether that name is bound to another value somewhere.

**LB10.** The rejection was measured with a single local `val`; the form of the error message for a top-level `val` and for a `val` that is a class property was not verified in this lesson.

## The Same Constant, Two Variables

Now the measurement of the lesson. There are four observations. `val-literal`: two separate `val` variables are bound to the same `"kotlin"` constant. `var-literal`: a `var` is compared with the same constant. `val-built` and `var-built` build the same text at run time, one with `joinToString` and the other with `buildString`. In this way we measure whether it makes a difference that the declaration form is `val` or `var`.

In every observation, `Criterion` takes three things: the measured value, a twin built the same way, and a different value. In the constant observations, the twin is a second variable or the constant itself. In the built observations, the twin is produced by calling the same method once more; so both sides are created separately at run time. The different value is each time the string `kotlim`, with one letter changed.

```kotlin
// Main.kt — the same literal in two variables
fun main() {
    val first = "kotlin"
    val twin = "kotlin"
    var changeable = "kotlin"
    val built = listOf("kot", "lin").joinToString("")
    var builtVar = buildString {
        append("kot")
        append("lin")
    }
    val checks = listOf(
        Criterion("val-literal", first, twin, "kotlim"),
        Criterion("var-literal", changeable, "kotlin", "kotlim"),
        Criterion("val-built", built, listOf("kot", "lin").joinToString(""), "kotlim"),
        Criterion("var-built", builtVar, buildString {
            append("kot")
            append("lin")
        }, "kotlim"),
    )
    for (check in checks) {
        check.report()
    }
    summarize(checks)
}
```

```
val-literal: same-object=true, twin=true, other=false -> singleton
var-literal: same-object=true, twin=true, other=false -> singleton
val-built: same-object=false, twin=true, other=false -> content
var-built: same-object=false, twin=true, other=false -> content
singleton: 2
identity: 0
content: 2
partial-content: 0
```

`val-literal` is singleton: `first` and `twin` are the same object, `same-object=true`. The same string constant is held as a single object during compilation, and every expression that uses it reaches the same object. `var-literal` is singleton too; using `var` does not break the constant being the same object, because `var` affects only whether the name can be reassigned. `val-built` and `var-built` are content: `joinToString` and `buildString` produce a new string at run time, so `same-object=false`; with the twin `==` still gives `true`, and with the different value `false`.

The summary lines show two singleton and two content. So binding a value with `val` or with `var` does not decide the criterion; what decides it is where the value comes from: a constant, or a value built at run time.

**LB11.** That a string constant is a single object was measured on the JVM target of Kotlin 2.4.21; this behavior rests on the JVM's string pool, and it is not claimed to be the same on other targets.

**LB12.** The built strings were measured with `joinToString` and `buildString`; that other ways of building give the same result was not measured in this lesson.

## val Does Not Freeze the Bound Object

Above, we said for `val` that "the name can never be bound to another value again." The limit of this sentence is that the bound object itself may change. The block below builds a mutable list declared with `val` and adds an element to it. It then shows a `val` whose first value is not given on the declaration line but assigned afterward.

```kotlin
// Main.kt — val can change the bound object, the first value can be given later
fun main() {
    val names = mutableListOf("ann")
    names.add("bob")
    println(names)
    val assigned: Int
    val flag = names.size > 1
    if (flag) {
        assigned = 10
    } else {
        assigned = 20
    }
    println(assigned)
}
```

```
[ann, bob]
10
```

`names` is a `val`; even so, the `add` call turns the list into `[ann, bob]`. Nothing raises an error, because the name `names` is still bound to the same list; what changed is the inside of the list. The second variable, `assigned`, receives no value on its declaration line, its type is written, and it is assigned exactly once in each of the `if` and `else` branches. The compiler accepts this pattern and prints `10` on the screen. The rule is this: a `val` variable can receive a value at most once on every path of its scope.

A common mistake is to read the word `val` as "the object cannot be changed." If you want to freeze the contents of a list, a read-only collection type is chosen (`listOf`); `val` alone does not provide this. Java comparison: a `final` variable in Java carries the same limit, an element can be added to a `final` list, and a `final` variable whose first value is given afterward is also a known pattern in Java. These two Java behaviors were not measured here; they were mentioned as known behavior.

**LB13.** In this section the measurement used `MutableList` and `Int`; whether the properties change when a `val` is bound to a `data class` instance was not measured in this lesson.

## Changing a var from Inside a Lambda

Kotlin allows a lambda to read and change a `var` variable outside itself. The block below updates a total and a call counter from inside `forEach`; then it copies the current value of the total into a `val` and changes the original `var` variable.

```kotlin
// Main.kt — changing a var from inside a lambda
fun main() {
    var total = 0
    var calls = 0
    listOf(1, 2, 3).forEach {
        total += it
        calls++
    }
    println("total: $total, calls: $calls")
    val snapshot = total
    total = 100
    println("copy: $snapshot, var: $total")
}
```

```
total: 6, calls: 3
copy: 6, var: 100
```

The first line gives `total: 6, calls: 3`: the lambda ran three times and both variables outside were updated. The second line shows that the copy stayed separate. The value of `snapshot` stays `6`, while `total` becomes `100`. Because an `Int` value is copied at the moment it is assigned, a `var` that changes afterward is not reflected in the copy.

The reason for the mechanism is that the lambda captures the outside variable not as a value but by carrying the variable itself; the lambda and the outside code work on the same `var`. In Java, for the same job, the local variable that the lambda captures must be effectively `final`, so a container such as an array or an `AtomicInteger` is used to keep a counter. In Kotlin there is no need for such a workaround. This Java rule and how Kotlin carries the captured variable behind the scenes were not measured in this lesson; what was measured is only that the outside `var` values are current after the lambda.

**LB14.** The captured variable was of type `Int` and was measured with `forEach`; the behavior of the same update in a multi-threaded context is outside this lesson.

## const val and Concatenation

At the start of the lesson, we said of `const val` "a constant whose value is known during compilation." This definition has a consequence: the concatenation of two `const val` strings is also computed during compilation. The block below compares the concatenation of two `const val` with a top-level `val` that builds the same text at run time with `joinToString`. In both measurements the twin comes from writing the same expression once more.

```kotlin
// Main.kt — const val concatenation and a run-time value
const val PREFIX = "kot"
const val FULL = PREFIX + "lin"

val runtimeFull = listOf("kot", "lin").joinToString("")

fun main() {
    val checks = listOf(
        Criterion("const-concat", FULL, "kotlin", "kotlim"),
        Criterion("runtime-top-level", runtimeFull, "kotlin", "kotlim"),
    )
    for (check in checks) {
        check.report()
    }
    summarize(checks)
}
```

```
const-concat: same-object=true, twin=true, other=false -> singleton
runtime-top-level: same-object=false, twin=true, other=false -> content
singleton: 1
identity: 0
content: 1
partial-content: 0
```

`const-concat` is singleton: the expression `PREFIX + "lin"` turns into the constant `"kotlin"` at compile time and is bound to the single object in the string pool. `runtime-top-level` is content: when the same text is built at run time, it becomes a separate string, `same-object=false`. The summary line shows one singleton and one content.

The common mistake here is to treat `const val` and a top-level `val` variable as interchangeable. Both stand at the top of the file and neither can be reassigned; but one takes its value during compilation and the other while the program runs. The boundary case follows from this: a `const val` is built only with values the compiler can compute. Java comparison: a `static final` field in Java carries a similar distinction, since there are `static final` fields that are compile-time constants and ones that are not. This Java distinction was not measured.

**LB15.** This comparison was measured with two `const val` strings and `joinToString`; the behavior of numeric `const val` concatenations was not measured in this section.

## Habits That Come from the Measurements

In this lesson's measurements, the difference between `val` and `var` appeared as the difference between binding a variable and the contents of an object. A `val` means a name bound once; if the bound object is a mutable list, an element can be added to the list, and the output `[ann, bob]` showed this. So saying "immutable" for a `val` calls for care: what cannot be reassigned is the binding, not the object. To protect the contents, a read-only collection type or `val` fields inside a `data class` are used.

When a `var` was changed from inside a lambda, the counter was visible outside too: `total: 6, calls: 3`. The measurement showed that the lambda and the outside code look at the same variable; it is known that underneath this lies the variable being wrapped in a box, but this was not measured in this lesson. The copied value, on the other hand, stayed separate: in the line `copy: 6, var: 100`, the copy was not affected when the `var` was reassigned. A common mistake is to think that when a value is captured and the variable is changed later, the captured value changes too; the difference between a copy and a reference has to be told apart by looking at where the assignment is made.

A `const val` is a compile-time constant and can take only a primitive type or a string value; it is defined at the top level, in an object declaration, or in a companion object. In the measurement, the concatenation of two `const val` appeared as a single object, while a concatenation computed at run time gave a value equal in content but separate. The practical rule is this: if a value can be known at compile time, write `const val`; if it cannot, write `val`. In Java, the same distinction is expressed with `static final`, but whether a field is a compile-time constant depends on its initializer expression and cannot be read from the declaration.

If these three observations had to be reduced to one sentence: ask whether a name changes separately from whether the object the name is bound to changes. Each time you see a `val` while reading code, keeping these two questions apart catches a large part of mistakes in advance.

## The Bounding Measurement

The singleton criterion is limited to the constant. When the same text is built at run time, the criterion turns back to content: `val-built` and `var-built` are two examples of this. The same `"kotlin"` text falls under the singleton criterion when it is written as a constant in one place, and under the content criterion when it is built with `joinToString` or `buildString` in another. For this reason, when comparing text in code, `==` is used instead of `===`; the former gives the right answer in both cases.

## Summary

- `val` is a variable that cannot be reassigned, and `var` is one that can; the type of both can be found by inference.
- `const val` is a compile-time constant; its value must be known during compilation.
- A second assignment to a `val` variable is rejected by the compiler.
- Choosing `val` or `var` does not decide the equality criterion; whether the value comes from a constant or from run time does.
- This lesson's criterion: **singleton**; bounding measurement: the same text built at run time falls under the content criterion.

## Next Step

The type of a variable was found by inference. In the next lesson we look at basic types: numbers, the character, and the logical value. The same number `1` is an `Int` in one context and a `Long` in another; we will measure why an `==` that mixes these two types is rejected.
