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"
}
