---
title: 'Classes and Constructors'
source: 'https://academia.sh/en/courses/kotlin/classes-and-constructors'
course: 'The Kotlin Language'
language: en
updated: '2026-10-21T05:02:40+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures the execution order of property initializers, the init block, and the secondary constructor body'
    - 'Shows the behavior of parameters with default values and of a custom accessor definition'
    - 'Measures that instances with the same values, built by two routes from a class without equals, fall to the identity criterion'
    - 'Verifies with the bounding measurement that equality is determined by the equals methods that are written, not by the class'
---

# Classes and Constructors

A measurement of the execution order of the primary constructor, the init block, and the secondary constructor, and of why instances of a class without equals fall to the identity criterion.

The previous lesson showed that a class without `equals` on the right side of the Elvis operator turned the criterion into identity. This lesson looks at how those classes are written: the primary constructor, the `init` block, the secondary constructor, and properties. First it measures which part runs when, then it explains through the criterion why two instances with the same values are not counted as equal.

The measurement axis stays the same: the relationship between a value and another value that counts as equal to it, the **criterion**. The `Criterion` class is given again below, as it was in the opening lesson. The rule works in order: if the first value and its twin are the same object, the criterion is singleton; if not, and the twin is not equal, it is identity; if the twin is equal but the third value is not, it is content; if the third value is equal too, it is partial content. This lesson measures the identity slice and moves on to content and partial content in the bounding measurement.

```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 Execution Order of Constructors

In Kotlin, the primary constructor of a class is written in the class header: `class Member(val name: String, val level: Int)`. The `val` in front of a parameter defines both the parameter and a property of the same name. The body can contain properties with initializers and `init` blocks; these behave like the body of the primary constructor and run in the order they are written in the file. A secondary constructor is written with the `constructor` keyword and has to delegate to the primary constructor with `: this(...)`. The constructor it delegates to runs first, and the secondary constructor's own body runs last.

The block below makes this order readable from the output. The `trace` function prints a line every time it is called. The initializer of the `title` property calls `trace`, the `init` block prints a line, and the body of the secondary constructor prints a line. `Member("ada")` calls the secondary constructor, and `Member("can", 2)` calls the primary constructor directly.

```kotlin
// Main.kt — constructor order
fun trace(label: String): String {
    println("step: $label")
    return label
}

class Member(val name: String, val level: Int) {
    val title = trace("property")

    init {
        println("init: $name")
    }

    constructor(name: String) : this(name, 1) {
        println("secondary: $name")
    }
}

fun main() {
    val first = Member("ada")
    println(first.level)
    val second = Member("can", 2)
    println(second.level)
}
```

```
step: property
init: ada
secondary: ada
1
step: property
init: can
2
```

In both constructions, the `step: property` line comes before the `init: ...` line, because `title` is written before the block. The secondary constructor runs only in the first construction, and the `secondary: ada` line is printed after the `init: ada` line: the primary constructor and its initializers are completed before the body of the secondary constructor. There is no `secondary` line in the second construction, because the primary constructor was called directly.

**TN11.** This measurement shows the order only for this class. Swapping the initializer and the `init` block changes the output; the order is the order of writing. Which initializer comes before which `init` block depends on how the measured class is written.

## Properties, Default Values, and Accessors

The `val` and `var` parameters in the primary constructor become properties. Properties also have a computed kind: `val area: Int get() = width * height` is not stored in a field and is computed every time it is read. Parameters can be given default values, and the caller can also pass parameters by name, out of order. If a custom `set` is written for a `var` property, the assigned value can be processed before it goes into the field; the field itself is reached by the name `field`.

The block below measures three things. `Rectangle(3)` leaves the height at the default value 1. `Rectangle(height = 2, width = 5)` passes the parameters by name, out of order. Finally, a string with spaces at the start and end is assigned to the `label` property; `set` strips those spaces.

```kotlin
// Main.kt — properties
class Rectangle(val width: Int, val height: Int = 1) {
    val area: Int get() = width * height

    var label: String = "unnamed"
        set(value) {
            field = value.trim()
        }
}

fun main() {
    val narrow = Rectangle(3)
    println(narrow.area)
    val wide = Rectangle(height = 2, width = 5)
    println(wide.area)
    wide.label = "  box  "
    println("[" + wide.label + "]")
}
```

```
3
10
[box]
```

The areas were computed as 3 times 1 and 5 times 2. The label was stored not as it was assigned but after passing through `set`: the square brackets show that no spaces remain.

**TN12.** `area` is not stored in a field. The measured block shows only the result of reading; that `area` would change if `width` and `height` were mutable does not follow from this block. This was measured only for `val` values that come from the constructor.

## A Class Without Equals

Now back to the criterion. The `Coordinate` class has two constructors: the primary constructor takes two numbers, and the secondary constructor splits a string of the form `"3,4"` and delegates to the same primary constructor. The class has no `equals`. In Kotlin every class derives from `Any`, and `Any`'s `equals` method looks at whether two references point to the same object. Unless the class overrides it, the `==` operator looks at identity too.

```kotlin
// Coordinate.kt — a class without equals
class Coordinate(val x: Int, val y: Int) {
    constructor(text: String) : this(text.substringBefore(",").toInt(), text.substringAfter(",").toInt())
}
```

The measurement consists of three lines. In the first, the first two of the three values are the same object. In the second, two separate `Coordinate(3, 4)` are built from the primary constructor. In the third, the twin is built from the secondary constructor, that is, from the string; the values are 3 and 4 again.

```kotlin
// Main.kt — instances built by two routes
fun main() {
    val same = Coordinate(1, 2)
    val checks = listOf(
        Criterion("same-object", same, same, Coordinate(1, 3)),
        Criterion("same-route", Coordinate(3, 4), Coordinate(3, 4), Coordinate(3, 5)),
        Criterion("two-routes", Coordinate(3, 4), Coordinate("3,4"), Coordinate(3, 5)),
    )
    for (check in checks) check.report()
    summarize(checks)
}
```

```
same-object: same-object=true, twin=true, other=false -> singleton
same-route: same-object=false, twin=false, other=false -> identity
two-routes: same-object=false, twin=false, other=false -> identity
singleton: 1
identity: 2
content: 0
partial-content: 0
```

In the first line the rule stops at the first question: because it is the same object, the criterion is singleton. In the second and third lines, `twin=false` comes out even though the fields are exactly the same, and the criterion is identity. The construction route, whether primary or secondary, does not change the result. What makes two instances with the same values equal is not the class's fields; it is what `equals` does.

This lesson's measurement has to be completed with a distinction. What is measured here is whether two instances are counted as equal to each other. Whether an instance is shared from somewhere else, or whether a change is visible through another reference, is a separate question and was not measured in this lesson. The identity criterion says only this: `==` gives `false` for two separate objects.

**TN13.** The measurement is only for `Coordinate` and the two constructor routes. The same result was not measured for a class whose fields are of other types, or for a class derived from a superclass other than `Any`. If the superclass has overridden `equals`, the picture may be different.

## Constructor Order in Inheritance

When a class derives from another class, one more link is added to the constructor order. Kotlin classes are closed by default; the class to be derived from is marked `open`, and the subclass calls the superclass's constructor in the header line. In the block below, both the superclass and the subclass have an `init` block. Which one runs first is read from the output.

```kotlin
// Main.kt — superclass constructor
open class Base(val name: String) {
    init {
        println("Base init: $name")
    }
}

class Derived(name: String, val extra: Int) : Base(name) {
    init {
        println("Derived init: $extra")
    }
}

fun main() {
    val item = Derived("ada", 3)
    println(item.name + item.extra)
}
```

```
Base init: ada
Derived init: 3
ada3
```

The output shows the order clearly: the `Base init: ada` line comes before the `Derived init: 3` line. The `Base(name)` call in the subclass's header line builds the superclass before the subclass's own initializers run. The reason is simple: the subclass's state can depend on the superclass's state, so the side being depended on has to be ready first. The last line shows that the `name` value comes from the superclass and the `extra` value from the subclass, and that both are reached through a single object.

This order extends the execution order in the previous section. In a single class, the order is property initializers, `init` blocks, and the secondary constructor body; in inheritance, the whole of the superclass is added in front of this. If a property of the subclass is touched inside the superclass's constructor, that property has not been set up yet; for this reason superclass constructors do not read state that belongs to the subclass. The limit of responsibility is visible in this output: each class sets up its own fields only in its own `init` block.

## Computed on Every Read and a One-Time Initializer

It was said earlier that the `area` property is not stored in a field and is computed every time it is read. This has a measurable consequence: how many times is the computing function called? In the block, two properties call the same function. One is defined with an initializer, the other with `get()`. Both are read three times; the counters keep how many times the function ran.

```kotlin
// Main.kt — computed on every read
var storedCalls = 0
var computedCalls = 0

fun compute(kind: String): Int {
    if (kind == "stored") storedCalls++ else computedCalls++
    return 10
}

class Holder {
    val stored: Int = compute("stored")
    val computed: Int get() = compute("computed")
}

fun main() {
    val holder = Holder()
    println(holder.stored + holder.stored + holder.stored)
    println(holder.computed + holder.computed + holder.computed)
    println("stored=$storedCalls computed=$computedCalls")
}
```

```
30
30
stored=1 computed=3
```

The last line of the output gives the counters: `stored=1 computed=3`. Both properties were read three times and both gave the total `30`, but the computing function ran a different number of times. The property defined with an initializer computes the value once, while the object is being built, and stores it in a field. The property defined with `get()` carries no field; the function runs again on every read.

Since both results return the same value, the difference is visible only from the counter, not from the output. For this reason, a property defined with `get()` suits values that are cheap and have to be up to date every time: a field derived from other properties, or a total. If an expensive computation or a function with a side effect is put into `get()`, every read pays that cost and the side effect is repeated as many times as there are reads. If the computation has to be done once, the initializer is chosen; if it has to be up to date every time, the accessor is chosen.

## A Written Equals and Other Types

A hand-written `equals` does not meet only values of the same type. Because its parameter is `Any?`, `null` and unrelated types can come in as well. The `other is ValueCoordinate` check we write filters out these cases. The block calls `equals` four times: with `null`, with a string, with another instance with the same values, and with the object itself.

```kotlin
// Main.kt — equals and other types
class ValueCoordinate(val x: Int, val y: Int) {
    override fun equals(other: Any?): Boolean =
        other is ValueCoordinate && x == other.x && y == other.y

    override fun hashCode(): Int = 31 * x + y
}

fun main() {
    val point = ValueCoordinate(1, 2)
    val anything: Any = "1,2"
    println(point.equals(null))
    println(point.equals(anything))
    println(point == ValueCoordinate(1, 2))
    println(point.equals(point))
}
```

```
false
false
true
true
```

All four calls return a `Boolean`, and the output is `false`, `false`, `true`, `true`. `equals(null)` gives `false`, because the `is` check fails for `null`. When the string is passed as an `Any`, `false` comes out too; because the types do not match, the fields are not even looked at. On a separate instance with the same values `true` returns, and on the object itself `true` returns again.

The parameter being `Any?` is a contract: `equals` can meet any kind of value and must say `false` without throwing an exception. A common mistake is to assume the parameter is `ValueCoordinate` and convert it directly, or to skip the type check and reach for the fields. The correct pattern is the `is` check first; thanks to the `&&` chain, the field comparison is made only after the check passes, and the compiler sees the `other` on the right side as a `ValueCoordinate`. `hashCode` is computed from the same fields; equal objects must produce equal hash codes, otherwise hash-based collections work inconsistently.

## The Bounding Measurement

If the written `equals` determines equality and not the class's fields, classes with the same fields should fall to different criteria depending on `equals`. The bounding measurement tests this with three lines. `ValueCoordinate` writes an `equals` that looks at both fields and a `hashCode` consistent with it. `LeftCoordinate` looks only at the `x` field. Of the three lines, the first tries full equality, the second the case where `x` is the same and `y` differs, and the third the case where `x` differs. The two lines at the end of the block put two instances with the same values into a set with `hashSetOf` and print the size of the set; this is how it observes that `hashCode` is written along with `equals`. The `hashCode` values themselves are not printed.

```kotlin
// Main.kt — once equals is written
class ValueCoordinate(val x: Int, val y: Int) {
    override fun equals(other: Any?): Boolean =
        other is ValueCoordinate && x == other.x && y == other.y

    override fun hashCode(): Int = 31 * x + y
}

class LeftCoordinate(val x: Int, val y: Int) {
    override fun equals(other: Any?): Boolean = other is LeftCoordinate && x == other.x

    override fun hashCode(): Int = x
}

fun main() {
    val checks = listOf(
        Criterion("full", ValueCoordinate(3, 4), ValueCoordinate(3, 4), ValueCoordinate(3, 5)),
        Criterion("x-only-y-differs", LeftCoordinate(3, 4), LeftCoordinate(3, 4), LeftCoordinate(3, 5)),
        Criterion("x-only-x-differs", LeftCoordinate(3, 4), LeftCoordinate(3, 4), LeftCoordinate(2, 4)),
    )
    for (check in checks) check.report()
    summarize(checks)
    println(hashSetOf(Coordinate(3, 4), Coordinate(3, 4)).size)
    println(hashSetOf(ValueCoordinate(3, 4), ValueCoordinate(3, 4)).size)
}
```

```
full: same-object=false, twin=true, other=false -> content
x-only-y-differs: same-object=false, twin=true, other=true -> partial-content
x-only-x-differs: same-object=false, twin=true, other=false -> content
singleton: 0
identity: 0
content: 2
partial-content: 1
2
1
```

The same class pattern gives two criteria across three lines. Because `ValueCoordinate` compares both fields, the twin and the other value separate: content. `LeftCoordinate` comes out equal even when `y` differs, so the third value gives the same result as the twin and the criterion falls to partial content; when `x` differs, content comes out. Two separate lines of the same class give two separate criteria; so the criterion is determined not by a class but by the fields `equals` looks at and the pair of values being compared, together. In the sets, `Coordinate` stays as two separate elements, and `ValueCoordinate` comes down to a single element.

**TN14.** This measurement is for a small class with two fields. In a class with more fields, which fields enter `equals` is specific to each class; the same behavior cannot be assumed without measuring.

## Summary

- The primary constructor is written in the class header; initializers and `init` blocks run in the order they are written, and the secondary constructor's body runs last.
- A secondary constructor has to delegate to the primary constructor with `: this(...)`, and the constructor it delegates to completes first.
- Properties can be given behavior with default values, named arguments, a computed read, and a custom `set`.
- Instances with the same values of a class without `equals` fall to the identity criterion, whichever constructor they were built with.
- This lesson's criterion: **identity**. In the bounding measurement the same class pattern fell to content and to partial content as well, depending on `equals`.

## Next Step

To make a class behave like a value, `equals`, `hashCode`, and `toString` had to be written by hand. The next lesson measures data classes, which generate these three from the properties in the primary constructor: it looks at whether a separate object built with `copy` is counted as equal, and at what a destructuring declaration does.
