---
title: 'Object Declarations'
source: 'https://academia.sh/en/courses/kotlin/object-declarations'
course: 'The Kotlin Language'
language: en
updated: '2026-10-21T05:02:40+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Writes an object declaration and measures that a value read twice is a single object'
    - 'Separates the companion object (`companion object`) from its factory function'
    - 'Reads the `toString` output and the equality of a `data object` declaration'
    - 'Explains that an object declaration and an object expression behave differently although they are written with the same keyword'
---

# Object Declarations

Measures the object declaration, the companion object, the `data object`, and the object expression with the criterion that makes two values equal, and shows that a declaration is a singleton while an expression builds a new object on every evaluation.

In the previous lesson, "Sealed Types and Enumerations", we saw that enumeration entries and `data object` values are singletons: reading the same entry twice returned the same object. This lesson goes down to the source of that singleton behavior. The `object` keyword shows up in Kotlin in three separate places: in a declaration, inside a class as a companion object, and at the start of an expression. All three are written with the same keyword; this lesson measures whether the three do the same thing.

The measure is the same question in every lesson of this course: what makes these two values equal? There are four possible answers: they are the same object (singleton), equality rests on identity (identity), their contents are equal (content), and only some of their contents are equal (partial content).

An **object declaration** defines a type and builds the single instance of that type at the same time. The name of the declaration is the name of both the type and that single value. The constructor is not called; writing the name is reading the value itself.

## The Measurement Setup

`Criterion` stays in the first block, in the same form, in every lesson. Each observation takes three values: `first`, `twin` (a value built separately with the same recipe), and `other` (a value that differs in only one respect). The rule sees the result, not the reason: if `first === twin`, the criterion is `singleton`; if not, and `first == twin` is false, it is `identity`; if `first == other` is false, it is `content`; if none of these, it is `partial-content`.

```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 }}")
    }
}
```
**TN21.** In an object declaration `twin` cannot be built separately, because there is no second instance; in this lesson we gave as `twin` the value of the same declaration read a second time. As `other` we used a declaration with the same body but a different name.

## A Declaration Is a Single Object

The block below writes two object declarations. `IdSource` keeps a counter; `OtherSource` is a separate declaration with the same body. `IdSource` is read into two separate variables, and the counter is advanced through these two variables and through its name directly.

```kotlin
// Main.kt — an object declaration read twice
object IdSource {
    private var next = 1
    fun take(): Int = next++
}

object OtherSource {
    private var next = 1
    fun take(): Int = next++
}

fun main() {
    val a = IdSource
    val b = IdSource
    println(a.take())
    println(b.take())
    println(IdSource.take())
    Criterion("object-declaration", a, b, OtherSource).report()
}
```

```
1
2
3
object-declaration: same-object=true, twin=true, other=false -> singleton
```

The three `take()` calls advanced the same counter, whichever variable they were made through, because `a` and `b` point to the same object. The rule wrote this as `singleton`: since `===` came out true, the equality of the twin was not even looked at. The counter of `OtherSource` is separate, so it is not equal to the first object.

Because a declaration is not a constructor call, putting parentheses after the name does not work either. We verified this first: the fragment below did not compile on Kotlin 2.4.21, which is why it stays without output.

```kotlin
object IdSource

fun main() {
    val source = IdSource()
}
```

**TN22.** We do not write the error text of the rejection in the lesson; it was only verified that the fragment does not compile. Whether a declaration can inherit from another class or implement an interface was not measured in this lesson.

## The Companion Object and the Factory

A **companion object** is an object declaration written inside a class as `companion object`. It holds the members that can be called with the class name. Since it is a declaration, it is a singleton. But a factory function written inside it builds instances of the class, and those instances are not singletons. We measure the two side by side.

```kotlin
// Main.kt — the companion object is a singleton, the factory builds a new object every time
class Account private constructor(val owner: String) {
    companion object {
        fun open(owner: String): Account = Account(owner)
    }
}

fun main() {
    val checks = listOf(
        Criterion("companion-object", Account.Companion, Account.Companion, Any()),
        Criterion("factory-result", Account.open("ada"), Account.open("ada"), Account.open("can")),
    )
    for (check in checks) check.report()
    summarize(checks)
}
```

```
companion-object: same-object=true, twin=true, other=false -> singleton
factory-result: same-object=false, twin=false, other=false -> identity
singleton: 1
identity: 1
content: 0
partial-content: 0
```

The `companion-object` line came out `singleton` and the `factory-result` line came out `identity`. Two `object` counterparts written inside the same class gave two separate criteria: the companion object itself is one, and the accounts it produces are separate on every call. Even when the owners of two `Account` objects are the same, `==` did not return true, because `Account` is an ordinary class and does not redefine `equals`; equality falls to identity. In the language of the rule, this means `identity`. Making the constructor private (`private constructor`) narrowed the way of building instances to the function of the companion object; the factory thus became the only entry door.

**TN23.** That the factory builds a new `Account` on every call was measured here only for this class. It is also possible for a factory to return an instance built beforehand; in that case the criterion would be `singleton`, but we did not write such a factory in this lesson.

## data object

A `data object` adds to an object declaration the readable `toString` and the `equals`/`hashCode` of a data class. We do not print the default `toString` output of a plain `object` in this course; a `data object`, on the other hand, writes only its name. This is the reason state objects in sealed hierarchies are written as `data object`: a readable name shows up in the output.

```kotlin
// Main.kt — a data object carries a readable toString
data object Accepted

fun main() {
    println(Accepted)
    println(Accepted == Accepted)
    val checks = listOf(Criterion("data-object", Accepted, Accepted, "Accepted"))
    for (check in checks) check.report()
}
```

```
Accepted
true
data-object: same-object=true, twin=true, other=false -> singleton
```

The first line of the output contains nothing but the name of the declaration. The criterion is `singleton` again: a `data object` does not produce a new instance, it only adds readability to the existing singleton object. The `"Accepted"` string we gave as `other` is not equal to `Accepted`; even though the two are spelled the same, their types are separate.

## Creation on First Access and the Interface

When is the single instance of an object declaration built? When the program starts, or at the moment it is first used? The answer to this question shows up in the `init` block in the body of the declaration: whenever the block runs, the instance has been built at that time. A declaration can also implement an interface, that is, bind its behavior to a contract like an ordinary class. Finally, a declaration can be matched in a `when` expression like a constant.

In the block below, `Polite` is an object declaration that implements the `Greeter` interface and prints a line when it is built. `Idle` is a `data object`. The `describe` function matches a value in three ways: equality with `Idle`, a type check with `is Polite`, and everything else. `main` prints `started` on its first line; this way, where the `init` line shows up reveals at which moment the declaration was built.

```kotlin
// Main.kt — the object is built on first access, implements an interface, matches with when
interface Greeter {
    fun greet(name: String): String
}

object Polite : Greeter {
    init {
        println("polite created")
    }

    override fun greet(name: String): String = "hello " + name
}

data object Idle

fun describe(state: Any): String = when (state) {
    Idle -> "empty"
    is Polite -> "polite"
    else -> "other"
}

fun main() {
    println("started")
    val greeter: Greeter = Polite
    println(greeter.greet("ada"))
    println(Polite.greet("can"))
    println(describe(Idle))
    println(describe(Polite))
    println(describe(Any()))
    Criterion("interface-object", greeter, Polite, Idle).report()
}
```

```
started
polite created
hello ada
hello can
empty
polite
other
interface-object: same-object=true, twin=true, other=false -> singleton
```

The `polite created` line showed up after `started` and before the first `greet` call. `Polite` was not built at the start of the program: the declaration was built the moment its name was first accessed, and the `init` block ran at that time. The second access did not print a new line, because the instance already existed. `greeter` and `Polite` pointed to the same object, and the criterion came out `singleton`; even though the type of the `greeter` variable is `Greeter`, the object underneath did not change.

`describe` gave three separate results. `empty` was written for `Idle`, `polite` for `Polite`, and `other` for an ordinary `Any()`. Since an object declaration is a singleton, it matches like a constant inside `when`: the `Idle ->` line comes down to the `Idle == state` comparison. `is Polite` worked too, because a declaration defines both a value and a type; for a type with a single instance, the two give the same result.

The practical conclusion from this measurement has two sides. First, leaving a side effect in the `init` block, that is, printing a line or opening a resource, ties the moment the declaration is built to the flow of the program; which line runs first depends on the place of the first access. Second, when an object declaration implements an interface, every variable typed with that interface still carries the same single instance; the criterion therefore does not differ by type, it stays bound to the object itself.

**TN24.** The first access to an object declaration being made from several threads at the same time was not measured in this lesson; only the order of creation in a single flow was observed.

## The Reading Rule from the Measurements

Up to this point we measured lines, and every line answered the same question: do two reads give the same object? Putting the lines side by side, a reading rule comes out. A declaration produces a value when its name is written; `Stamp`, `Polite`, `Accepted`, and `Idle` came out `singleton` for this reason. An expression builds a new object every time it is evaluated; `makeMarker()` came out `identity` for this reason. The factory of a class is a function, not a declaration; `Account.open` built a new `Account` on every call, and the result was again `identity`. The rule looks at where the keyword is written: if a name goes to a single value, it is a singleton; if a call or an expression builds a new value, it is identity.

A common mistake is to turn this rule around. A reader who sees the `object` keyword says "this is a singleton"; but the `object : Type { ... }` form is an expression. The opposite is wrong too: when a `companion object` is written inside a class, what the companion object produces is taken to be a singleton. The measurement showed that the companion object itself is one, while what its factory produces is separate. For this reason, state bound to a singleton object, for example a counter or a cache, is shared by all parts of the program; state bound to an object produced by a factory belongs only to the owner of that object.

The moment of creation is a second detail. The `polite created` line showed up on the first access and not on the second. The creation cost or side effect of a singleton object is paid not at the start of the program but at the place where it is first used. This is useful when you need to work out when what you write in an `init` block will run: the order depends on which code accesses it first, and if that code moves, the line moves too. For this reason it is wise to keep the `init` block short, to set up only fields, and not to produce output; in this lesson we printed output only to make the measurement visible.

The third detail is the `when` expression. Matching with `Idle` and the type check with `is Polite` do the same job, because the declaration has a single instance. In a type with a single instance, the questions "is this value equal to that value" and "is this value of that type" give the same answer. In a class where two instances are possible, these two questions would part ways; the absence of this parting is an observable consequence of being a singleton.

## Predicting Before Measuring

Predicting the criterion before running a line tests whether you really understand the rule. The `Criterion` setup asks three questions: is `===` true when the same object is given, is `==` true when the twin is given, is `==` true when a different object is given? For an object declaration the first question is always answered true, because the name goes to a single value; so the twin must be the same object too, and the third question comes out false only when a different type is given. The criterion is written as `singleton`.

For an object expression, even the first question is answered false. Calling the same function twice gives two separate objects, and the twins are not counted as equal to each other; when both are `false`, the criterion becomes `identity`. You can put every line in this lesson on one of these two paths: if the name points to a declaration, `singleton`; if it points to a call or an expression, `identity`. Even the `data object` line did not step outside this rule; what the `data` prefix added was not equality but a readable text.

The place where a prediction fails is the place where you remembered a rule incompletely. For example, if you knew that `describe` would write `empty` for `Idle` but expected `other` instead of `polite` for `Polite`, it means you thought that the `is` check does not work with the name of a declaration. The measurement corrected this assumption: a declaration is both a value and a type.

## The Bounding Measurement

An **object expression** is the use of the `object` keyword at the start of an expression: `object { ... }`, or, after a type name, `object : Type { ... }`. It carries no name, and it builds a new object every time the expression is evaluated. The difference between a declaration and an expression is exactly here: the same keyword produces a single object in a declaration and a new object every time in an expression. This is why the measure of this course separates them.

```kotlin
// Main.kt — an object expression builds a new object on every evaluation
object Stamp

fun makeMarker(): Any = object {
    val side = 4
}

fun main() {
    val checks = listOf(
        Criterion("declaration", Stamp, Stamp, makeMarker()),
        Criterion("expression", makeMarker(), makeMarker(), makeMarker()),
    )
    for (check in checks) check.report()
    summarize(checks)
}
```

```
declaration: same-object=true, twin=true, other=false -> singleton
expression: same-object=false, twin=false, other=false -> identity
singleton: 1
identity: 1
content: 0
partial-content: 0
```

The `declaration` line came out `singleton` and the `expression` line came out `identity`. Although the expression has no name and therefore no constructor, every `makeMarker()` call built a new object, and since these objects do not redefine `equals`, they did not come out equal to one another. This measurement is a boundary: it is not enough to assume that something is a singleton when you see the `object` keyword; you need to look at whether the keyword stands in a declaration or in an expression.

**TN25.** How a variable captured by an object expression appears, and the expression extending a type, were not measured in this lesson; only a nameless, typeless expression was used.

## Summary

- An object declaration builds a type and the single instance of that type together; writing the name is reading the value, and two reads give the same object.
- A companion object is an object declaration inside a class; it is a singleton, but its factory can build the instances of the class separately on every call.
- A `data object` adds only a readable `toString` and equality to the singleton object; it does not change the criterion.
- An object expression builds a new object on every evaluation; the same `object` keyword is a singleton in a declaration and identity in an expression.
- This lesson's criterion: **singleton**. When two reads give the same object, `===` comes out true and the reason for the equality is not sought.

## Next Step

In this lesson we looked at whether an object is built as one or anew every time, and we kept the behavior only in the body of the class itself. The next lesson separates behavior from a type: interfaces write a contract, and a class can hand it over to another object. Whether handing over also hands over equality will be measured.
