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
- Create a `ConfigManager` object that reads configuration from a file and provides thread-safe access
- Implement a `ShapeFactory` using companion objects that creates different geometric shapes
- Build an event system using object expressions for event handlers
- Create a caching system using an object declaration with TTL (time-to-live) functionality
Quiz
- What's the difference between object declarations and object expressions?
- When is an object instance created in Kotlin?
- Can companion objects implement interfaces?
Show Answers
- Object declarations create named singletons; object expressions create anonymous objects for one-time use.
- Object instances are created lazily when first accessed, not at application startup.
- Yes, companion objects can implement interfaces and be used polymorphically.