Kotlin - Object Declarations

Overview

Kotlin's object declarations provide powerful ways to create singletons, companion objects, and anonymous objects. This tutorial covers object declarations, object expressions, companion objects, and their practical applications in real-world scenarios.

๐ŸŽฏ Learning Objectives:
  • Understand object declarations and singleton pattern
  • Learn about object expressions for anonymous objects
  • Master companion objects and static-like functionality
  • Apply object patterns in real-world scenarios
  • Choose appropriate object types for different use cases

Object Declarations

Object declarations create singleton instances - classes with exactly one instance that's created lazily when first accessed.

Basic Object Declaration

object DatabaseManager {
    private var isConnected = false
    
    fun connect() {
        if (!isConnected) {
            println("Connecting to database...")
            isConnected = true
        }
    }
    
    fun disconnect() {
        if (isConnected) {
            println("Disconnecting from database...")
            isConnected = false
        }
    }
    
    fun isConnected() = isConnected
}

fun main() {
    DatabaseManager.connect()
    println("Connected: ${DatabaseManager.isConnected()}")
    DatabaseManager.disconnect()
}
Key Point: Object declarations are thread-safe singletons. The object is created lazily on first access and only one instance ever exists.

Object with Properties and Initialization

object AppConfig {
    const val APP_NAME = "MyApp"
    const val VERSION = "1.0.0"
    
    private val properties = mutableMapOf()
    
    init {
        println("AppConfig initialized")
        loadDefaultConfig()
    }
    
    private fun loadDefaultConfig() {
        properties["theme"] = "dark"
        properties["language"] = "en"
        properties["auto_save"] = "true"
    }
    
    fun getProperty(key: String): String? = properties[key]
    
    fun setProperty(key: String, value: String) {
        properties[key] = value
    }
    
    fun getAllProperties(): Map = properties.toMap()
}

fun main() {
    println("App: ${AppConfig.APP_NAME} v${AppConfig.VERSION}")
    println("Theme: ${AppConfig.getProperty("theme")}")
    AppConfig.setProperty("theme", "light")
    println("Updated theme: ${AppConfig.getProperty("theme")}")
}

Object Expressions

Object expressions create anonymous objects, similar to anonymous inner classes in Java but more powerful.

Simple Object Expression

fun createTemporaryObject() = object {
    val name = "Temporary Object"
    val timestamp = System.currentTimeMillis()
    
    fun display() = println("$name created at $timestamp")
}

fun main() {
    val temp = createTemporaryObject()
    temp.display()
    println("Name: ${temp.name}")
}

Object Expression Implementing Interface

interface ClickListener {
    fun onClick()
    fun onDoubleClick() = println("Double clicked") // Default implementation
}

class Button(private val text: String) {
    private var clickListener: ClickListener? = null
    
    fun setOnClickListener(listener: ClickListener) {
        clickListener = listener
    }
    
    fun click() {
        println("Button '$text' clicked")
        clickListener?.onClick()
    }
    
    fun doubleClick() {
        println("Button '$text' double-clicked")
        clickListener?.onDoubleClick()
    }
}

fun main() {
    val button = Button("Save")
    
    // Object expression implementing interface
    button.setOnClickListener(object : ClickListener {
        override fun onClick() {
            println("Saving document...")
        }
        
        override fun onDoubleClick() {
            println("Quick save activated!")
        }
    })
    
    button.click()
    button.doubleClick()
}

Object Expression with Multiple Interfaces

interface Drawable {
    fun draw()
}

interface Clickable {
    fun click()
}

fun createInteractiveShape() = object : Drawable, Clickable {
    private val color = "blue"
    
    override fun draw() = println("Drawing a $color shape")
    override fun click() = println("Shape was clicked!")
    
    fun getInfo() = "Interactive $color shape"
}

fun main() {
    val shape = createInteractiveShape()
    shape.draw()
    shape.click()
    println(shape.getInfo())
}

Companion Objects

Companion objects provide a way to create static-like members and factory methods in Kotlin classes.

Basic Companion Object

class MathUtils {
    companion object {
        const val PI = 3.14159
        const val E = 2.71828
        
        fun add(a: Int, b: Int) = a + b
        fun multiply(a: Int, b: Int) = a * b
        
        fun factorial(n: Int): Long {
            return if (n <= 1) 1 else n * factorial(n - 1)
        }
    }
    
    // Instance members
    fun calculate(x: Double) = x * PI
}

fun main() {
    // Access companion object members
    println("PI = ${MathUtils.PI}")
    println("5 + 3 = ${MathUtils.add(5, 3)}")
    println("5! = ${MathUtils.factorial(5)}")
    
    // Create instance for instance members
    val utils = MathUtils()
    println("Area of circle with radius 5: ${utils.calculate(5.0)}")
}

Named Companion Objects

class User private constructor(val name: String, val email: String) {
    
    companion object Factory {
        private val emailRegex = Regex("""\S+@\S+\.\S+""")
        
        fun create(name: String, email: String): User? {
            return if (isValidEmail(email)) {
                User(name, email)
            } else {
                null
            }
        }
        
        fun createAdmin(name: String): User {
            return User(name, "[email protected]")
        }
        
        private fun isValidEmail(email: String) = emailRegex.matches(email)
    }
    
    override fun toString() = "User(name='$name', email='$email')"
}

fun main() {
    val user1 = User.create("John", "[email protected]")
    val user2 = User.Factory.createAdmin("Admin") // Can use companion name
    val invalidUser = User.create("Jane", "invalid-email")
    
    println(user1) // User(name='John', email='[email protected]')
    println(user2) // User(name='Admin', email='[email protected]')
    println(invalidUser) // null
}

Companion Object with Interface Implementation

interface JsonSerializer {
    fun serialize(obj: T): String
    fun deserialize(json: String): T?
}

data class Person(val name: String, val age: Int) {
    companion object : JsonSerializer {
        override fun serialize(obj: Person): String {
            return """{"name":"${obj.name}","age":${obj.age}}"""
        }
        
        override fun deserialize(json: String): Person? {
            // Simplified JSON parsing
            val nameMatch = Regex(""""name":"([^"]+)"""").find(json)
            val ageMatch = Regex(""""age":(\d+)""").find(json)
            
            return if (nameMatch != null && ageMatch != null) {
                Person(nameMatch.groupValues[1], ageMatch.groupValues[1].toInt())
            } else {
                null
            }
        }
    }
}

fun main() {
    val person = Person("Alice", 30)
    val json = Person.serialize(person)
    println("JSON: $json")
    
    val deserializedPerson = Person.deserialize(json)
    println("Deserialized: $deserializedPerson")
}

Real-World Examples

Logger Singleton

object Logger {
    private val logs = mutableListOf()
    
    enum class Level { DEBUG, INFO, WARNING, ERROR }
    
    fun log(level: Level, message: String) {
        val timestamp = System.currentTimeMillis()
        val logEntry = "[$timestamp] ${level.name}: $message"
        logs.add(logEntry)
        println(logEntry)
    }
    
    fun debug(message: String) = log(Level.DEBUG, message)
    fun info(message: String) = log(Level.INFO, message)
    fun warning(message: String) = log(Level.WARNING, message)
    fun error(message: String) = log(Level.ERROR, message)
    
    fun getAllLogs(): List = logs.toList()
    
    fun clearLogs() {
        logs.clear()
        println("Logs cleared")
    }
}

fun main() {
    Logger.info("Application started")
    Logger.debug("Debug information")
    Logger.warning("This is a warning")
    Logger.error("An error occurred")
    
    println("\nAll logs:")
    Logger.getAllLogs().forEach { println(it) }
}

Factory Pattern with Companion Objects

sealed class DatabaseConnection {
    abstract fun connect(): String
    abstract fun disconnect()
    
    companion object {
        fun create(type: String, config: Map): DatabaseConnection? {
            return when (type.lowercase()) {
                "mysql" -> MySQLConnection(config)
                "postgresql" -> PostgreSQLConnection(config)
                "sqlite" -> SQLiteConnection(config)
                else -> null
            }
        }
    }
}

class MySQLConnection(private val config: Map) : DatabaseConnection() {
    override fun connect() = "Connected to MySQL at ${config["host"]}:${config["port"]}"
    override fun disconnect() = println("Disconnected from MySQL")
}

class PostgreSQLConnection(private val config: Map) : DatabaseConnection() {
    override fun connect() = "Connected to PostgreSQL at ${config["host"]}:${config["port"]}"
    override fun disconnect() = println("Disconnected from PostgreSQL")
}

class SQLiteConnection(private val config: Map) : DatabaseConnection() {
    override fun connect() = "Connected to SQLite database: ${config["file"]}"
    override fun disconnect() = println("Disconnected from SQLite")
}

fun main() {
    val mysqlConfig = mapOf("host" to "localhost", "port" to "3306")
    val sqliteConfig = mapOf("file" to "app.db")
    
    val mysql = DatabaseConnection.create("mysql", mysqlConfig)
    val sqlite = DatabaseConnection.create("sqlite", sqliteConfig)
    
    println(mysql?.connect())
    println(sqlite?.connect())
    
    mysql?.disconnect()
    sqlite?.disconnect()
}

Registry Pattern with Object Declaration

object ServiceRegistry {
    private val services = mutableMapOf()
    
    inline fun  register(service: T) {
        val key = T::class.simpleName ?: throw IllegalArgumentException("Cannot determine service type")
        services[key] = service
    }
    
    inline fun  get(): T? {
        val key = T::class.simpleName ?: return null
        return services[key] as? T
    }
    
    fun listServices(): List = services.keys.toList()
    
    fun clear() = services.clear()
}

// Example services
class EmailService {
    fun sendEmail(to: String, subject: String, body: String) {
        println("Sending email to $to: $subject")
    }
}

class NotificationService {
    fun notify(message: String) {
        println("Notification: $message")
    }
}

fun main() {
    // Register services
    ServiceRegistry.register(EmailService())
    ServiceRegistry.register(NotificationService())
    
    println("Registered services: ${ServiceRegistry.listServices()}")
    
    // Use services
    val emailService = ServiceRegistry.get()
    val notificationService = ServiceRegistry.get()
    
    emailService?.sendEmail("[email protected]", "Welcome", "Welcome to our service!")
    notificationService?.notify("New user registered")
}

Object vs Class Comparison

When to Use Objects vs Classes

// Use object for singletons, utilities, constants
object StringUtils {
    fun capitalize(str: String) = str.replaceFirstChar { it.uppercase() }
    fun reverse(str: String) = str.reversed()
}

// Use class for multiple instances
class HttpClient(private val baseUrl: String) {
    fun get(endpoint: String) = "GET $baseUrl$endpoint"
    fun post(endpoint: String, data: String) = "POST $baseUrl$endpoint: $data"
}

// Use companion object for factory methods and constants
class ApiResponse(val data: T, val status: Int) {
    companion object {
        fun  success(data: T) = ApiResponse(data, 200)
        fun  error(status: Int) = ApiResponse(null as T, status)
        
        const val STATUS_OK = 200
        const val STATUS_ERROR = 500
    }
}

fun main() {
    // Object - single instance
    println(StringUtils.capitalize("hello"))
    
    // Class - multiple instances
    val client1 = HttpClient("https://api.example.com/")
    val client2 = HttpClient("https://api.test.com/")
    
    // Companion object - factory methods
    val successResponse = ApiResponse.success("Data loaded")
    val errorResponse = ApiResponse.error(ApiResponse.STATUS_ERROR)
}

Advanced Patterns

Object Expression with Captured Variables

fun createCounter(start: Int = 0) = object {
    private var count = start
    
    fun increment() = ++count
    fun decrement() = --count
    fun get() = count
    fun reset() { count = start }
}

fun main() {
    val counter1 = createCounter()
    val counter2 = createCounter(100)
    
    repeat(3) { counter1.increment() }
    println("Counter 1: ${counter1.get()}") // 3
    
    repeat(2) { counter2.decrement() }
    println("Counter 2: ${counter2.get()}") // 98
}

Companion Object Extensions

class Person(val name: String, val age: Int) {
    companion object
}

// Extension function for companion object
fun Person.Companion.createChild(parentName: String) = Person("${parentName} Jr.", 0)

fun Person.Companion.createAdult(name: String) = Person(name, 18)

fun main() {
    val child = Person.createChild("John")
    val adult = Person.createAdult("Jane")
    
    println("${child.name} is ${child.age} years old")
    println("${adult.name} is ${adult.age} years old")
}

Best Practices

Guidelines for Object Usage

  • Object Declarations: Use for singletons, utilities, and stateless operations
  • Object Expressions: Use for one-time anonymous implementations
  • Companion Objects: Use for factory methods, constants, and static-like functionality
  • Thread Safety: Object declarations are thread-safe by default
  • Memory: Objects are created lazily and remain in memory until the application ends

Common Anti-patterns to Avoid

// โŒ Don't use object for data that changes frequently
object BadCounter {
    var count = 0 // Mutable state in object - potential issues
}

// โœ… Use class for mutable state
class GoodCounter {
    var count = 0
}

// โŒ Don't use companion object for instance-specific behavior
class BadExample {
    private val instanceId = generateId()
    
    companion object {
        fun doSomething() {
            // Cannot access instanceId here!
        }
    }
}

// โœ… Use regular methods for instance behavior
class GoodExample {
    private val instanceId = generateId()
    
    fun doSomething() {
        println("Instance $instanceId doing something")
    }
}

private fun generateId() = System.currentTimeMillis()

Key Takeaways

  • Object declarations create thread-safe singletons with lazy initialization
  • Object expressions create anonymous objects that can implement interfaces
  • Companion objects provide static-like functionality and factory methods
  • Objects are created once and remain in memory for the application lifetime
  • Use objects for stateless utilities, singletons, and factory patterns
  • Companion objects can implement interfaces and be extended

Practice Exercises

  1. Create a `ConfigManager` object that reads configuration from a file and provides thread-safe access
  2. Implement a `ShapeFactory` using companion objects that creates different geometric shapes
  3. Build an event system using object expressions for event handlers
  4. Create a caching system using an object declaration with TTL (time-to-live) functionality

Quiz

  1. What's the difference between object declarations and object expressions?
  2. When is an object instance created in Kotlin?
  3. Can companion objects implement interfaces?
Show Answers
  1. Object declarations create named singletons; object expressions create anonymous objects for one-time use.
  2. Object instances are created lazily when first accessed, not at application startup.
  3. Yes, companion objects can implement interfaces and be used polymorphically.