Kotlin Objects — Singletons, Companion Objects, and Anonymous Classes
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
Trying to instantiate an object declaration: Object declarations are already singletons. You cannot call the constructor (there is none). Access members directly.
Confusing companion objects with regular objects: Companion objects are tied to a class and accessed via the class name. Regular objects are standalone singletons.
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.
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.
Creating platform-specific objects without expect/actual: For Kotlin Multiplatform, use expect and actual declarations instead of object expressions for platform-specific implementations.
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
- 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.
- 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.
- 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.
- Can object declarations have constructors?
Answer: No. Object declarations cannot have constructors. The singleton is created by the runtime on first access.
- 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
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