Kotlin Object Expressions and Companion Objects Explained
Kotlin's object keyword unifies three forms: singleton declarations, companion objects, and anonymous objects. Charlie uses companion object as a replacement for Java's static factory methods, and object declarations to replace singleton utility classes.
1. What You'll Learn
objectdeclarations: Kotlin singleton patterncompanion object: factory methods and static member replacement- Object expressions: anonymous implementations
- Companion object extensions: class-level extension functions
- Charlie in action:
Orderfactory +OrderValidatorsingleton
2. A Real Architect's Story
(1) Pain Point: Java Singletons and Static Methods Proliferation
Charlie's Java project had 10+ singleton classes (getInstance()) and a flood of static utility methods (static), making testing difficult and extension impossible.
(2) Kotlin object's Solution
KOTLIN
// Java: verbose singleton
public class OrderValidator {
private static final OrderValidator INSTANCE = new OrderValidator();
private OrderValidator() {}
public static OrderValidator getInstance() { return INSTANCE; }
}
// Kotlin: 1-line singleton
object OrderValidator {
fun validate(order: Order) = require(order.total >= 0)
}
A single line of Kotlin replaces Java's 5-line singleton boilerplate, and it's naturally thread-safe.
3. object Declaration (Singleton)
(1) Basic Syntax
KOTLIN
// Singleton: only one instance, thread-safe initialization
object OrderValidator {
fun validate(order: Order): Boolean {
return order.total >= 0 && order.id.startsWith("ORD-")
}
}
// Usage: access directly by object name
OrderValidator.validate(order) // No getInstance() needed
(2) Singletons with State
KOTLIN
object OrderCounter {
private var count = 0L
fun increment() { count++ }
fun getCount() = count
fun reset() { count = 0 }
}
OrderCounter.increment()
println(OrderCounter.getCount()) // 1
(3) object vs Java Singleton
| Dimension | Java Singleton | Kotlin object |
|---|---|---|
| Code volume | 5+ lines | 1 line |
| Thread safety | Must ensure manually | Naturally safe |
| Serialization | Requires special handling | Naturally correct |
| Testability | Difficult | Likewise difficult (global state) |
| Alternative | Dependency injection | Dependency injection (recommended) |
4. companion object
(1) Basic Syntax
KOTLIN
class Order(val id: String, val total: Double, val status: String) {
companion object {
// Factory methods
fun create(id: String, total: Double): Order =
Order(id, total, "PENDING")
fun fromCsv(line: String): Order {
val parts = line.split(",")
return Order(parts[0], parts[1].toDouble(), parts[2])
}
// Constants
const val DEFAULT_STATUS = "PENDING"
const val MAX_TOTAL = 1_000_000.0
}
}
// Usage: like static in Java
val order = Order.create("ORD-001", 299.99)
val csvOrder = Order.fromCsv("ORD-002,1500.00,CONFIRMED")
println(Order.DEFAULT_STATUS)
(2) Named Companion Objects
KOTLIN
class Order(val id: String, val total: Double) {
companion object Factory {
fun create(id: String, total: Double) = Order(id, total)
}
}
// Can use either name
Order.create("ORD-001", 299.99)
Order.Factory.create("ORD-001", 299.99)
(3) companion object Implementing Interfaces
KOTLIN
interface JsonFactory<T> {
fun fromJson(json: String): T
}
class Order(val id: String, val total: Double) {
companion object : JsonFactory<Order> {
override fun fromJson(json: String): Order {
// Parse json and create Order
return Order("ORD-from-json", 0.0)
}
}
}
// Polymorphic usage
val factory: JsonFactory<Order> = Order
val order = factory.fromJson("{\"id\":\"ORD-001\"}")
(4) companion object vs Java static
| Dimension | Java static | Kotlin companion object |
|---|---|---|
| Nature | Class-level member | Object (companion singleton) |
| Implementing interfaces | Cannot | Can |
| Extension functions | Cannot | Can |
| Naming | Unnamed | Can be named |
| Runtime existence | Does not exist | Exists (singleton object) |
5. Object Expressions (Anonymous Objects)
(1) Basic Syntax
KOTLIN
// Anonymous object: like Java anonymous inner class
val handler = object : OrderHandler {
override fun handle(order: Order) {
println("Handling ${order.id}")
}
}
// Anonymous object implementing multiple interfaces
val multiHandler = object : OrderHandler, Loggable {
override fun handle(order: Order) { println("Handle: ${order.id}") }
override fun log(message: String) { println("[LOG] $message") }
}
(2) Anonymous Objects Capturing External Variables
KOTLIN
fun createHandler(prefix: String): OrderHandler {
var count = 0 // Captured by the anonymous object
return object : OrderHandler {
override fun handle(order: Order) {
count++
println("$prefix [#$count] ${order.id}")
}
}
}
(3) Three object Forms Compared
| Form | Syntax | Lifecycle | Use Case |
|---|---|---|---|
object declaration |
object Name { } |
Process-level singleton | Global utilities / configuration |
companion object |
class X { companion object { } } |
Class-level singleton | Factory methods / constants |
| Object expression | object : Interface { } |
Temporary instance | One-off implementations |
6. Practical Use Case Comparison
| Scenario | Recommended Form | Example |
|---|---|---|
| Global configuration | object declaration |
object AppConfig |
| Factory methods | companion object |
Order.create(...) |
| One-off callbacks | Object expression | object : OrderHandler { } |
| Constant collections | companion object + const |
const val MAX = 1_000_000 |
| Utility functions | object declaration or top-level function |
object OrderUtils |
| Multi-interface implementation | Object expression | object : A, B { } |
7. Companion Object Extensions
KOTLIN
class Order(val id: String, val total: Double) {
companion object // Empty - exists as extension target
}
// Extension on companion object
fun Order.Companion.fromCsv(line: String): Order {
val parts = line.split(",")
return Order(parts[0], parts[1].toDouble())
}
// Usage
val order = Order.fromCsv("ORD-001,299.99")
💡 Hint: Companion object extensions let third-party libraries add "static methods" to a class without modifying the class source code.
7. object Form Selection Flow
flowchart TD
A[Need object?] --> B{Strongly tied to a class?}
B -->|Yes| C{Need factory/constants?}
B -->|No| D{Need one-time instance?}
C -->|Yes| E[companion object]
C -->|No| F{Need global singleton?}
D -->|Yes| G[Object Expression<br/>object : Interface]
D -->|No| H[Regular class instance]
F -->|Yes| I[object Declaration<br/>Singleton]
F -->|No| H
8. Complete Example
KOTLIN
// ============================================
// OrderProcessor - Object Patterns
// Feature: Singleton, Companion Factory, Anonymous Handler
// ============================================
data class Order(val id: String, val total: Double, var status: String, val customer: String) {
companion object {
const val DEFAULT_STATUS = "PENDING"
const val MAX_TOTAL = 1_000_000.0
fun create(id: String, total: Double, customer: String): Order {
require(total >= 0) { "Total must be non-negative" }
require(total <= MAX_TOTAL) { "Total exceeds maximum" }
return Order(id, total, DEFAULT_STATUS, customer)
}
fun bulkCreate(vararg ids: String, total: Double, customer: String): List<Order> {
return ids.map { create(it, total, customer) }
}
}
}
// Singleton validator
object OrderValidator {
private val rules = mutableListOf<(Order) -> Boolean>()
fun addRule(rule: (Order) -> Boolean) { rules.add(rule) }
fun validate(order: Order): List<String> {
val errors = mutableListOf<String>()
if (!order.id.startsWith("ORD-")) errors.add("Invalid ID format")
if (order.total < 0) errors.add("Negative total")
rules.forEach { rule ->
if (!rule(order)) errors.add("Custom rule failed")
}
return errors
}
}
// Singleton counter
object OrderMetrics {
private var totalProcessed = 0L
private var totalRevenue = 0.0
@Synchronized fun record(order: Order) {
totalProcessed++
totalRevenue += order.total
}
fun report() = "Processed: $totalProcessed orders, Revenue: \$$totalRevenue USD"
}
interface OrderHandler {
fun handle(order: Order): String
}
fun main() {
// Companion factory
println("=== Factory Creation ===")
val order1 = Order.create("ORD-001", 299.99, "Alice")
val order2 = Order.create("ORD-002", 15_000.00, "Bob")
val bulkOrders = Order.bulkCreate("ORD-010", "ORD-011", "ORD-012", total = 500.0, customer = "Charlie")
println(order1)
println(order2)
bulkOrders.forEach { println(it) }
// Singleton validator
println("\n=== Validation ===")
OrderValidator.addRule { it.total > 0 }
val validOrder = Order("ORD-100", 299.99, "PENDING", "Alice")
val invalidOrder = Order("BAD-001", -50.0, "PENDING", "Bob")
println("Valid: ${OrderValidator.validate(validOrder)}")
println("Invalid: ${OrderValidator.validate(invalidOrder)}")
// Singleton metrics
println("\n=== Metrics ===")
listOf(order1, order2).forEach { OrderMetrics.record(it) }
println(OrderMetrics.report())
// Anonymous handler
println("\n=== Anonymous Handler ===")
val shippingHandler = object : OrderHandler {
private var shipped = 0
override fun handle(order: Order): String {
shipped++
order.status = "SHIPPED"
return "Shipped ${order.id} (Total shipped: $shipped)"
}
}
println(shippingHandler.handle(order1))
println(shippingHandler.handle(order2))
}
Output:
TEXT
=== Factory Creation ===
Order(id=ORD-001, total=299.99, status=PENDING, customer=Alice)
Order(id=ORD-002, total=15000.0, status=PENDING, customer=Bob)
Order(id=ORD-010, total=500.0, status=PENDING, customer=Charlie)
Order(id=ORD-011, total=500.0, status=PENDING, customer=Charlie)
Order(id=ORD-012, total=500.0, status=PENDING, customer=Charlie)
=== Validation ===
Valid: []
Invalid: [Invalid ID format, Negative total, Custom rule failed]
=== Metrics ===
Processed: 2 orders, Revenue: $15299.99 USD
=== Anonymous Handler ===
Shipped ORD-001 (Total shipped: 1)
Shipped ORD-002 (Total shipped: 2)
❓ FAQ
Q Can object singletons accept dependency injection?
A Technically yes (via
var properties), but it's not recommended. Singleton global state makes testing difficult. For production code, prefer DI frameworks (Koin/Dagger) over object singletons.Q Are companion object members actually static?
A On the JVM, methods annotated with
@JvmStatic inside a companion object are compiled as true static methods. Without the annotation, they remain instance methods on the companion object.Q Can anonymous objects access external variables?
A Yes. Unlike Java anonymous inner classes, Kotlin anonymous objects can access and modify external
var variables (like closures).Q How do I choose between companion object and object declaration?
A Use
companion object for things tightly coupled to a class (factory methods, constants). Use object declarations for things independent of any class (global utilities, configuration).Q What's the difference between
const val and plain val in a companion object?A
const val is a compile-time constant (inlined at call sites), applicable only to primitives and String. Plain val is initialized at runtime.Q Can a companion object have a supertype?
A Yes. Companion objects can implement interfaces, making the factory pattern more elegant:
companion object : JsonFactory<Order>.📖 Summary
objectdeclarations create thread-safe singletons, replacing Java's 5+ lines of singleton boilerplatecompanion objectreplaces Javastatic, with more power (interfaces, extensions, naming)- Object expressions
object : Interface { }replace Java anonymous inner classes - Companion object extensions let you add "static methods" without modifying source code
- Singletons are hard to test; prefer dependency injection over object singletons in production
const valfor compile-time constants vsvalfor runtime initialization
📝 Exercises
- Beginner (⭐): Create an
OrderCountersingleton withincrement()andgetCount()methods. Hint:object OrderCounter { ... } - Intermediate (⭐⭐): Add a
companion objectto theOrderclass with afromCsvfactory method that parses a CSV line to create an Order. Hint:companion object { fun fromCsv(line: String): Order { ... } } - Challenge (⭐⭐⭐): Implement companion object interface
JsonFactory<Order>+ companion object extensionOrder.Companion.fromXml(). Hint:companion object : JsonFactory<Order> { ... }+ top-level extension function



