Skip to content

Kotlin Objects — Singletons, Companion Objects, and Anonymous Classes

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Kotlin Objects. We cover key concepts, practical examples, and best practices to help you master this topic.

Kotlin object keyword creates singleton objects with object declarations, provides static-like members through companion objects, and defines anonymous classes with object expressions for one-time use implementations.

What You'll Learn

  • Declare singletons with object declarations
  • Use companion objects for static members and Factory methods
  • Write object expressions for anonymous classes
  • Compare object expressions with lambdas
  • Create nested objects and platform-specific declarations
  • Apply object patterns in real-world scenarios

Why It Matters

The object keyword solves three common problems with a unified syntax. Singletons replace the manual static instance pattern from Java. Companion objects provide a clean home for factory methods and constants. Object expressions replace anonymous inner classes with concise syntax. Using objects correctly reduces boilerplate, improves code organization, and ensures thread-safe singletons without synchronization code.

Real-World Use

DodaTech uses object declarations for app-wide configuration registries and logging facades. Companion objects provide factory methods for database entities. Object expressions serve as one-off comparators in Sorting Algorithms and as callbacks in event handlers.

Learning Path

flowchart LR
  A[Data Classes] --> B[Objects\nYou are here]
  B --> C[Sealed Classes]
  style B fill:#f90,color:#fff

Object Declarations (Singletons)

An object declaration creates a singleton: a class with exactly one instance, created lazily and thread-safely.

object AppConfig {
    val appName: String = "DodaTech Suite"
    var isDebugMode: Boolean = false
    
    fun load() {
        println("Loading configuration for $appName")
        isDebugMode = true
    }
    
    private val features = mutableListOf("auth", "logging", "sync")
    
    fun isFeatureEnabled(name: String): Boolean = name in features
}

fun main() {
    // Access directly by name - no instance creation needed
    println(AppConfig.appName)        // Output: DodaTech Suite
    println(AppConfig.isDebugMode)    // Output: false
    
    AppConfig.load()
    println(AppConfig.isDebugMode)    // Output: true
    println(AppConfig.isFeatureEnabled("auth"))  // Output: true
    println(AppConfig.isFeatureEnabled("billing"))  // Output: false
    
    // AppConfig is a singleton
    val ref1 = AppConfig
    val ref2 = AppConfig
    println(ref1 === ref2)  // Output: true (same instance)
}

Output: The AppConfig singleton is accessed directly. All references point to the same instance. Initialization happens the first time the object is accessed.

Object declarations cannot have constructors. The object is created lazily when first accessed, with Thread Safety guaranteed by the JVM.

Companion Objects

Companion objects replace Java's static members. They are singleton objects associated with a class.

class User(
    val id: Long,
    val username: String,
    val email: String
) {
    companion object {
        private var nextId = 1L
        
        // Factory methods
        fun create(username: String, email: String): User {
            return User(nextId++, username, email)
        }
        
        // Constants
        const val MAX_USERNAME_LENGTH = 20
        const val DEFAULT_ROLE = "viewer"
        
        // Validation
        fun validateUsername(name: String): Boolean {
            return name.length in 3..MAX_USERNAME_LENGTH
        }
        
        // Static-like method
        fun anonymous(): User = User(0, "anonymous", "anon@example.com")
    }
}

fun main() {
    val alice = User.create("alice", "alice@example.com")
    println("${alice.id}: ${alice.username}")  // Output: 1: alice
    
    val bob = User.create("bob", "bob@example.com")
    println("${bob.id}: ${bob.username}")  // Output: 2: bob
    
    println(User.MAX_USERNAME_LENGTH)      // Output: 20
    println(User.validateUsername("ab"))   // Output: false
    println(User.validateUsername("alice")) // Output: true
    
    val anon = User.anonymous()
    println("${anon.id}: ${anon.username}")  // Output: 0: anonymous
}

Output: Companion object members are accessed through the class name. They provide factory methods, constants, and utility functions.

Companion Object with Name

Companion objects can have an explicit name.

class Database {
    companion object Factory {
        private const val DEFAULT_URL = "jdbc:sqlite:default.db"
        
        fun connect(url: String = DEFAULT_URL): Database {
            println("Connecting to $url")
            return Database()
        }
        
        fun createInMemory(): Database {
            return connect("jdbc:sqlite::memory:")
        }
    }
}

fun main() {
    val db1 = Database.connect()
    val db2 = Database.Factory.connect("jdbc:postgresql://localhost/mydb")
    val db3 = Database.createInMemory()
}

Output: Named companion objects can be referenced by their name explicitly or accessed through the class name.

Object Expressions (Anonymous Classes)

Object expressions create anonymous class instances without declaring a named class.

interface ClickHandler {
    fun onClick()
    fun onLongClick(): Boolean = false
}

abstract class Logger {
    abstract fun log(message: String)
    fun logError(message: String) = log("ERROR: $message")
}

fun main() {
    // Object expression implementing an interface
    val buttonHandler = object : ClickHandler {
        override fun onClick() {
            println("Button clicked!")
        }
        
        override fun onLongClick(): Boolean {
            println("Button long-clicked")
            return true
        }
    }
    
    buttonHandler.onClick()       // Output: Button clicked!
    println(buttonHandler.onLongClick())  // Output: true and "Button long-clicked"
    
    // Object expression extending an abstract class
    val consoleLogger = object : Logger() {
        override fun log(message: String) {
            println("[LOG] $message")
        }
    }
    
    consoleLogger.log("App started")     // Output: [LOG] App started
    consoleLogger.logError("Disk full")  // Output: [LOG] ERROR: Disk full
    
    // Object expression with no supertype (just a one-time object)
    val adHoc = object {
        val x = 10
        val y = 20
        fun sum() = x + y
    }
    println(adHoc.sum())  // Output: 30
    
    // Object expression as a comparator
    data class Person(val name: String, val age: Int)
    
    val people = listOf(Person("Alice", 30), Person("Bob", 25), Person("Charlie", 35))
    val sorted = people.sortedWith(object : Comparator<Person> {
        override fun compare(a: Person, b: Person): Int = a.age.compareTo(b.age)
    })
    println(sorted.map { it.name })  // Output: [Bob, Alice, Charlie]
}

Output: Object expressions create anonymous instances. They can implement interfaces, extend classes, or have no supertype.

When to Use Object Expressions versus Lambdas

Use object expressions when you need:

  • An anonymous class with multiple methods
  • An anonymous class that extends a class (not just an interface)
  • Access to the object reference (e.g., for method calls on itself)

Use lambdas when you need:

  • A single-function callback
  • Functional interface SAM conversion

Nested Objects

Objects can be nested inside classes and other objects.

class NetworkManager {
    // Nested object for status codes
    object Status {
        const val OK = 200
        const val NOT_FOUND = 404
        const val SERVER_ERROR = 500
    }
    
    // Nested object for configuration
    object Config {
        const val TIMEOUT = 30
        const val MAX_RETRIES = 3
    }
    
    inner class Request(val path: String) {
        fun execute(): Int {
            println("Executing request to $path")
            return Status.OK
        }
    }
}

fun main() {
    println(NetworkManager.Status.OK)     // Output: 200
    println(NetworkManager.Config.TIMEOUT) // Output: 30
    
    val manager = NetworkManager()
    val request = manager.Request("/api/users")
    val result = request.execute()  // Output: Executing request to /api/users
    println(result)  // Output: 200
}

Output: Nested objects organize related constants and utilities within the enclosing class.

Object Expressions and Captured Variables

Object expressions can capture variables from their enclosing scope, similar to lambdas.

fun createCounter() = object {
    var count = 0
    fun increment() = count++
    fun current() = count
}

fun main() {
    val counter = createCounter()
    counter.increment()
    counter.increment()
    println(counter.current())  // Output: 2
    
    // Object expression capturing a variable
    var clickCount = 0
    val handler = object : ClickHandler {
        override fun onClick() {
            clickCount++
            println("Clicked $clickCount times")
        }
    }
    
    handler.onClick()  // Output: Clicked 1 times
    handler.onClick()  // Output: Clicked 2 times
    println(clickCount)  // Output: 2
}

Output: Object expressions capture mutable variables. Changes to captured variables are visible inside and outside the expression.

Common Mistakes

  1. Trying to instantiate an object declaration: Object declarations are already singletons. You cannot call the constructor (there is none). Access members directly.

  2. Confusing companion objects with regular objects: Companion objects are tied to a class and accessed via the class name. Regular objects are standalone singletons.

  3. Using object expressions for single-method callbacks: When implementing a single-method interface, use a lambda with SAM conversion instead of an object expression for more concise code.

  4. Forgetting that object declarations are lazy: The object is initialized on first access, not at class load time. This is fine for most cases but matters if initialization order is important.

  5. Creating platform-specific objects without expect/actual: For Kotlin Multiplatform, use expect and actual declarations instead of object expressions for platform-specific implementations.

  6. Overusing object declarations for mutable state: Object declarations are global singletons. Mutable state in singletons can cause thread-safety issues and makes testing harder.

Practice Questions

  1. What is the difference between an object declaration and a class?

Answer: An object declaration creates a singleton with exactly one instance, created lazily and accessed by name. A class can have multiple instances created via constructors.

  1. What is a companion object used for?

Answer: A companion object provides a place for static-like members: factory methods, constants, validation functions, and utility methods associated with a class.

  1. When would you use an object expression instead of a lambda?

Answer: Use an object expression when you need an anonymous class with multiple methods, when extending a class, or when you need to reference the object by its anonymous type.

  1. Can object declarations have constructors?

Answer: No. Object declarations cannot have constructors. The singleton is created by the runtime on first access.

  1. Challenge: Implement a Logger system with an object declaration for the global logger, companion objects for per-class logging, and an object expression for a test logger that captures messages in memory.

Answer:

interface Logger {
    fun info(message: String)
    fun error(message: String)
}

object GlobalLogger : Logger {
    override fun info(message: String) = println("[INFO] $message")
    override fun error(message: String) = println("[ERROR] $message")
}

class Service(private val name: String) {
    companion object LoggerFactory : Logger {
        private val prefix = "[SERVICE]"
        override fun info(message: String) = println("$prefix [INFO] $message")
        override fun error(message: String) = println("$prefix [ERROR] $message")
    }
    
    fun process() {
        LoggerFactory.info("Starting $name")
        LoggerFactory.error("Something went wrong")
    }
}

fun createTestLogger(): Logger {
    val messages = mutableListOf<String>()
    return object : Logger {
        override fun info(message: String) {
            messages.add("INFO: $message")
            println("[TEST] INFO: $message")
        }
        override fun error(message: String) {
            messages.add("ERROR: $message")
            println("[TEST] ERROR: $message")
        }
    }
}

fun main() {
    GlobalLogger.info("Application started")
    
    val service = Service("DataProcessor")
    service.process()
    
    val testLogger = createTestLogger()
    testLogger.info("Test message")
    testLogger.error("Test error")
}

Mini Project

Build a configuration system using objects. Requirements:

  • AppConfig object declaration for global settings (appName, version, debug mode)
  • FeatureFlag companion object for per-class feature toggles
  • DatabaseConfig object for connection parameters
  • A Logger interface with FileLogger and ConsoleLogger object implementations
  • An object expression in a test that captures all log messages
  • Use nested objects for organizing constants within related classes

This project demonstrates all three object use cases in a practical application.

FAQ

Are Kotlin object declarations thread-safe?

Yes. Object declarations are initialized lazily and thread-safely using the JVM's class initialization mechanism. No additional synchronization is needed.

Can a companion object implement an interface?

Yes. Companion objects can implement interfaces, and the containing class can also implement the interface, offering both instance and static dispatch.

Can object expressions access private members of the enclosing class?

Yes, if the object expression is defined inside the class and the class is marked as open or the expression is within the class body.

What is the difference between object and companion object?

A companion object is a specific object tied to a class, accessed via the class name. A regular object is a standalone singleton not attached to any class.

Can I use object declarations in Kotlin Multiplatform?

Yes, but for platform-specific implementations, use the expect/actual mechanism with object declarations in actual modules.

What's Next

After mastering objects, learn about sealed classes for restricted class hierarchies. You can also explore extensions to add functionality to existing classes.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro