1. Three forms of object in Kotlin
The word “object” in Kotlin sounds like we are about to open a portal into OOP universes where everything is an object, including your morning coffee and your deadline anxiety. In practice, the keyword object in Kotlin hides three different constructs, and each solves a very down-to-earth programmer problem: “I want a single instance,” “I want an API via the class name,” “I want an implementation right here, without a separate file.”
To make it easier to navigate, keep this table in mind (it is worth rereading in a couple of days — it suddenly becomes very obvious):
| Construct | What it creates | How many instances | How to access | When it fits |
|---|---|---|---|---|
|
single object (singleton) | 1 per program | |
shared service/utility/command without state or with small state |
|
an object “glued” to the class | 1 per class | |
factories, constants, parsing, “static” methods |
|
anonymous object (one-off) | usually 1 “per location” | store in a variable / pass to a function | quick “in-place” interface implementations |
If you want a very short analogy: class is a blueprint, Foo() is manufacturing a part from the blueprint, object Foo is “the only part that is already on the table,” and object : Interface { ... } is “I will quickly assemble a thing right now, and then I will not need it anymore.”
2. object declaration: one instance and a clear name
As a project grows, there is a temptation to create a “utils” folder and dump everything into it, like a desk drawer full of cables: seemingly useful, but later you cannot find anything. An object declaration is a more disciplined way to say: “this entity exists as a single instance, and it has a name.” It is an ideal tool for small services, configuration, simple registries, and especially for commands in CLI applications.
Minimal example: a singleton utility
Let’s start as simply as possible. Imagine that in our training CLI app (let it be a small expense tracker) we want to print messages in a consistent style: "INFO", "ERROR", and so on. Instead of threading a “printer” through the entire codebase, we can create a single object.
object Console {
fun info(text: String) {
println("[INFO] $text") // [INFO] ...
}
fun error(text: String) {
println("[ERROR] $text") // [ERROR] ...
}
}
Usage is just as simple:
fun main() {
Console.info("Application started") // [INFO] Application started
Console.error("Something went wrong") // [ERROR] Something went wrong
}
Notice an important detail: Console has no constructor; you do not write Console() — the object already exists. That is the point of the singleton form.
object can implement interfaces
In previous lectures, we agreed to think in terms of contracts. Let’s continue: in our expense tracker we may have commands "help", "add", "list", "exit". It is convenient to describe this with an interface.
interface Command {
val name: String
fun run(args: String): String
}
Now the best part: each command can be declared as an object, because the “help command” does not need to exist in 20 instances. One is enough.
object HelpCommand : Command {
override val name: String = "help"
override fun run(args: String): String {
return "Commands: help, add, list, exit"
}
}
Call site:
fun main() {
val text = HelpCommand.run("")
println(text) // Commands: help, add, list, exit
}
This style often suddenly makes a project cleaner: “here is the list of commands, here are their implementations.” And yes, these are still ordinary objects; you can put them into a collection.
object and state: possible, but be careful
Singletons tempt you to store state “somewhere globally.” Sometimes that is fine (for example, an ID counter for a demo app), but it is important to remember: global state loves surprises.
Let’s make a simple ID generator for expenses:
object IdGenerator {
private var next: Int = 1
fun newId(): Int {
val id = next
next += 1
return id
}
}
Let’s test:
fun main() {
println(IdGenerator.newId()) // 1
println(IdGenerator.newId()) // 2
}
This works, but remember the feeling: “a magic counter somewhere in one place.” If later you start testing the code or rerunning scenarios, you may unexpectedly get “why does my ID start at 57?” That is not a Kotlin bug — that is the cost of global state.
object as a nested entity
You can declare object not only at the top level of a file, but also inside a class (as a nested singleton). This is handy when you want to “attach” a utility to a specific type, but not make it a companion object.
For example, if we have formatting settings:
class MoneyFormatter {
object Defaults {
const val currency: String = "USD"
}
}
And usage:
fun main() {
println(MoneyFormatter.Defaults.currency) // USD
}
This looks a bit “verbose,” but it illustrates the idea well: object helps group meaning close to where it is needed.
3. companion object: API via the class name
If an object declaration is “one object by itself,” then a companion object is “one object that lives next to a class and looks like part of the class.” Historically, it addresses a perennial pain: in Java people are used to static, while Kotlin has no static keyword. Instead, Kotlin offers: “if you want ‘class methods,’ here is a companion object.”
Constants and “static” settings
Let’s start with the simplest: a companion object often holds constants and defaults.
class AppConfig {
companion object {
const val appName: String = "Expense Tracker"
const val defaultCurrency: String = "USD"
}
}
Usage:
fun main() {
println(AppConfig.appName) // Expense Tracker
println(AppConfig.defaultCurrency) // USD
}
You access companion object members via the class name. And that is exactly what people typically want from “static” things: they belong to the type, not to a specific instance.
Factory method: controlling object creation
Sometimes you want to forbid “creating an object however,” and allow it only through a neat function that normalizes input data.
Imagine we have a Token, and we want to guarantee it has no leading/trailing spaces.
class Token private constructor(val value: String) {
companion object {
fun fromRaw(raw: String): Token {
return Token(raw.trim())
}
}
}
Usage:
fun main() {
val t = Token.fromRaw(" abc ")
println(t.value) // abc
}
The key points here are quite “grown-up,” even though the code is small: we hid the constructor (private constructor) and provided one official creation path. This disciplines the code more than “just don’t forget to trim.”
Parsing strings into a model
In CLI apps, we parse input all the time. A companion object is a great place for such functions, because parsing relates to the type: “Expense can be created from a string.”
Let’s make a minimal expense model:
data class Expense(val id: Int, val title: String, val cents: Long) {
companion object {
fun fromUserInput(raw: String, id: Int): Expense? {
val parts = raw.trim().split(";")
if (parts.size != 2) return null
return Expense(id, parts[0].trim(), parts[1].trim().toLongOrNull() ?: return null)
}
}
}
Let’s test:
fun main() {
val e = Expense.fromUserInput("Coffee; 350", id = 1)
println(e) // Expense(id=1, title=Coffee, cents=350)
}
The idea matters most here: Expense.fromUserInput(...) looks like “the class’s way to create an instance,” and reads much better than some external function parseExpense(...) scattered across the project.
4. object : ...: a one-off implementation without a separate class
Sometimes you realize: “I need an object that implements an interface, but only here, in one place.” Creating a SomeStrategyImpl.kt file for 6 lines is like buying a separate refrigerator for one yogurt (though, of course, some people do that — but that is a different course).
That is where an object expression appears: object : Interface { ... }.
Implementing an interface “in place”
Let’s take our command interface:
interface Command {
val name: String
fun run(args: String): String
}
Now let’s create a command right inside main, without a separate class and without object SomeName:
fun main() {
val ping: Command = object : Command {
override val name: String = "ping"
override fun run(args: String): String = "pong"
}
println(ping.run("")) // pong
}
This is a “one-off command”: it exists exactly where we declared it. This approach works well for tests, prototypes, temporary stubs, and “I need behavior, but I have not decided how it will fit into the architecture yet” situations.
An anonymous object can inherit and implement multiple contracts
An object expression allows both inheriting from a class and implementing an interface — all in one place. This is useful when you want to slightly override behavior.
In our style (simpler and closer to practice) it looks like this:
open class BaseLogger {
open fun log(text: String) = println(text) // ...
}
fun main() {
val logger = object : BaseLogger() {
override fun log(text: String) {
println("[DBG] $text") // [DBG] ...
}
}
logger.log("hello") // [DBG] hello
}
Here we did not create class DebugLogger, because we need it only in one place.
Limitation of anonymous objects: the variable type “cuts off” access
This is a subtle point that beginners often stumble on: an anonymous object may have a “unique” method, but if you store it in a variable of type Any or an interface type, you will see only what exists in that type.
fun main() {
val obj = object {
val x: Int = 10
fun msg(): String = "x=$x"
}
println(obj.msg()) // x=10
val asAny: Any = obj
println(asAny.toString()) // ... (and msg() is no longer visible)
}
This is not “Kotlin being stubborn,” it is a basic typing rule: available members are determined by the variable’s type, not by what is “actually inside.”
5. How to choose an object form in an application
When you have three constructs in your head, you want a magic rule like “always do it this way.” But in programming, magic rules usually live next to magic bugs, so it is better to keep a clear selection logic.
Let’s imagine our expense tracker processes commands. We want “permanent” commands ("help", "exit") and one experimental one ("ping") that we are not ready to move into a separate file yet. Then the decision can look like this: permanent commands are object declarations, and the experimental one is object : ....
Here is the “skeleton” of such an approach:
import kotlin.system.exitProcess
object ExitCommand : Command {
override val name: String = "exit"
override fun run(args: String): String {
exitProcess(0)
}
}
And somewhere in main:
fun main() {
val ping = object : Command {
override val name: String = "ping"
override fun run(args: String): String = "pong"
}
println(ping.run("")) // pong
}
And companion object in this story is great for “creating from a string” or “shared type-level constants.” For example, we can parse an expense via Expense.fromUserInput(...), and it reads like an “official creation path.” This is exactly when a companion object helps avoid multiplying global functions like parseXxx, which later are hard to find and even harder to maintain.
If you want to picture it as a diagram, it will look roughly like this:
flowchart TD
A["Need behavior"] --> B{"How many instances are needed?"}
B -->|One per program| C["object Foo { ... }"]
B -->|Bound to a class, called via the type name| D["companion object { ... }"]
B -->|One-off in a single place| E["object : Interface { ... }"]
Review questions
- How does object Foo differ from class Foo, and why can’t you write Foo() for an object?
- In what cases does companion object read better than a set of top-level functions?
- Why does Kotlin “lose” an anonymous object’s unique methods if you store it in a variable of type Any?
- Give an example where object : Interface { ... } is more appropriate than a separate class.
6. Typical mistakes when working with object and companion object
Mistake #1: turning an object into a “dump for everything.”
It is very easy to create object Utils and start putting everything there: date formatting, parsing, network calls, and “a temporary hack we will remove later.” As a result, the object becomes not a utility but the project’s black hole. The fix is simple: an object should have a clear, narrow responsibility and a good name (Console, IdGenerator, HelpCommand), not “everything about everything.”
Mistake #2: storing a lot of mutable state in a singleton without necessity.
An object looks like a convenient place for counters, caches, and “the last entered value.” The problem is that global state breaks predictability: a function’s behavior starts depending on what happened earlier. In tutorial examples (like IdGenerator) that can be acceptable, but in real code it is better to minimize var in singletons and keep them either immutable or very small and controlled.
Mistake #3: confusing object Foo and object : Foo.
object Foo is a named singleton object that you access by name (Foo.doIt()). object : Foo { ... } is an in-place anonymous object that most often has no name and exists as the value of an expression. Confusion leads to odd questions like “why can’t I import object : ...?” — because you can import a name, not a piece of an expression.
Mistake #4: adding “unique” methods to an anonymous object and then losing access due to the variable type.
A common situation: you write val x = object { fun debug() {} }, then you pass x somewhere as Any, and suddenly debug() disappears. This is expected: access is determined by the static type. If you truly need a unique method, you need an interface/class with that method, not an anonymous object “without a contract.”
Mistake #5: doing everything through companion object, even when it hurts readability.
Sometimes people start using companion object as a replacement for a utility module: “let everything live inside the class.” As a result, ExpenseParser suddenly lives inside Expense.Companion, Console hides inside App.Companion, and navigation becomes harder. A companion object is especially good where operations logically belong to the type: creation, parsing, constants, and factories. Everything else is better kept as separate entities with clear names.
GO TO FULL VERSION