How do I use object expressions and object declarations in Kotlin?

Object expressions vs. object declarations in Kotlin

Kotlin has two closely related features:

  • Object expressions: create an anonymous object immediately.
  • Object declarations: create a named singleton object.

They both use the object keyword, but they are used for different purposes.


1. Object expressions

Use an object expression when you need a one-off object, often to implement an interface or extend a class without creating a named class.

Basic anonymous object

fun main() {
    val user = object {
        val name = "Ava"
        val age = 30

        fun greet() {
            println("Hello, my name is $name")
        }
    }

    println(user.name)
    user.greet()
}

Here, user is an anonymous object with properties and functions.


2. Implementing an interface with an object expression

Object expressions are commonly used for callbacks, listeners, and small implementations.

interface ClickListener {
    fun onClick()
}

fun setClickListener(listener: ClickListener) {
    listener.onClick()
}

fun main() {
    setClickListener(object : ClickListener {
        override fun onClick() {
            println("Button clicked")
        }
    })
}

The syntax is:

object : SomeInterface {
    override fun someFunction() {
        // implementation
    }
}

3. Extending a class with an object expression

You can also create an anonymous subclass.

open class Animal(val name: String) {
    open fun speak() {
        println("$name makes a sound")
    }
}

fun main() {
    val dog = object : Animal("Buddy") {
        override fun speak() {
            println("$name barks")
        }
    }

    dog.speak()
}

4. Implementing multiple types

An object expression can extend one class and implement one or more interfaces.

open class Logger {
    open fun log(message: String) {
        println("Log: $message")
    }
}

interface Closeable {
    fun close()
}

fun main() {
    val resource = object : Logger(), Closeable {
        override fun log(message: String) {
            println("Custom log: $message")
        }

        override fun close() {
            println("Resource closed")
        }
    }

    resource.log("Started")
    resource.close()
}

If a superclass has a constructor, call it after the class name:

object : Logger()

5. Object declarations

Use an object declaration when you want a named singleton: exactly one instance, created lazily when first used.

object DatabaseConfig {
    val url = "jdbc:postgresql://localhost:5432/app"
    val username = "admin"

    fun connect() {
        println("Connecting to $url as $username")
    }
}

fun main() {
    DatabaseConfig.connect()
}

You do not instantiate it with DatabaseConfig().

Use it directly by name:

DatabaseConfig.connect()

6. Object declarations can implement interfaces

interface Analytics {
    fun track(event: String)
}

object ConsoleAnalytics : Analytics {
    override fun track(event: String) {
        println("Tracking event: $event")
    }
}

fun main() {
    ConsoleAnalytics.track("AppOpened")
}

This is useful for global services, registries, configuration, or strategy objects.


7. Object declarations can extend classes

open class AppLogger {
    open fun info(message: String) {
        println("INFO: $message")
    }
}

object Logger : AppLogger() {
    override fun info(message: String) {
        println("[App] $message")
    }
}

fun main() {
    Logger.info("Application started")
}

8. Companion objects

A companion object is an object declaration inside a class. It is commonly used for factory methods and static-like members.

class User private constructor(val name: String) {
    companion object {
        fun create(name: String): User {
            return User(name.trim())
        }
    }
}

fun main() {
    val user = User.create("  Ava  ")
    println(user.name)
}

You call companion object members through the class name:

User.create("Ava")

9. Named companion objects

A companion object can have a name.

class App {
    companion object Config {
        const val VERSION = "1.0.0"

        fun printVersion() {
            println("Version: $VERSION")
        }
    }
}

fun main() {
    App.printVersion()
    App.Config.printVersion()
}

Both calls are valid:

App.printVersion()
App.Config.printVersion()

10. Object expression visibility detail

If an anonymous object is stored in a local variable, you can access its members:

fun main() {
    val person = object {
        val name = "Mira"
        fun sayHi() = println("Hi, I am $name")
    }

    println(person.name)
    person.sayHi()
}

But if an anonymous object is returned from a public function, its specific members are not visible unless the return type exposes them.

class Factory {
    fun createPublic(): Any = object {
        val name = "Hidden"
    }

    private fun createPrivate() = object {
        val name = "Visible inside class"
    }

    fun demo() {
        val item = createPrivate()
        println(item.name)
    }
}

In this example:

fun createPublic(): Any = object {
    val name = "Hidden"
}

The caller only sees Any, not name.


11. Quick comparison

Feature Object expression Object declaration
Purpose One-off anonymous object Named singleton
Has a name? Usually no Yes
Instantiated with constructor? No No
Created when? Immediately where expression runs Lazily on first access
Common use Callbacks, temporary implementations Singletons, global config, shared services
Can extend classes? Yes Yes
Can implement interfaces? Yes Yes

Simple rule of thumb

Use an object expression when you need an object right here, right now:

val listener = object : ClickListener {
    override fun onClick() {
        println("Clicked")
    }
}

Use an object declaration when you need one shared named instance:

object AppSettings {
    val theme = "dark"
}

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.