Skip to main content

Kotlin Companion Objects

Introduction​

In object-oriented programming, you sometimes need functionality that belongs to a class rather than to instances of that class. In Java, you'd use static members for this purpose. Kotlin, however, doesn't have the static keyword. Instead, Kotlin provides companion objects as a more powerful and flexible alternative.

A companion object is a special object declared inside a class that allows you to:

  • Define constants and functions that are tied to the class, not to its instances
  • Access these members without creating an instance of the class
  • Implement interfaces and extend other classes
  • Hold factory methods and other class-level functionality

Let's explore how companion objects work in Kotlin and how they can enhance your programming experience.

Basic Syntax and Usage​

Declaring a Companion Object​

Here's how to declare a companion object within a class:

kotlin
class MyClass {
companion object {
val CONSTANT = "I am a constant"

fun classMethod() {
println("I'm a method that belongs to the class, not an instance")
}
}

fun instanceMethod() {
println("I'm a method that belongs to an instance")
}
}

Accessing Companion Object Members​

To access members of a companion object, you use the class name directly:

kotlin
fun main() {
// Accessing companion object members
println(MyClass.CONSTANT) // Output: I am a constant
MyClass.classMethod() // Output: I'm a method that belongs to the class, not an instance

// Accessing instance members requires an instance
val instance = MyClass()
instance.instanceMethod() // Output: I'm a method that belongs to an instance
}

Understanding Companion Objects​

Comparison with Java's Static Members​

Unlike Java's static members, companion objects are actual instances. They're singleton objects that are created when the containing class is loaded:

kotlin
class Calculator {
companion object {
val PI = 3.14159

fun add(a: Int, b: Int): Int {
return a + b
}
}
}

fun main() {
println("PI value: ${Calculator.PI}") // Output: PI value: 3.14159
println("Sum: ${Calculator.add(5, 3)}") // Output: Sum: 8
}

Named Companion Objects​

By default, companion objects don't have names, but you can name them if needed:

kotlin
class Book {
companion object BookFactory {
fun createBook(title: String): Book {
return Book().apply { this.title = title }
}
}

var title: String = ""
}

fun main() {
val book = Book.createBook("Kotlin Programming")
println(book.title) // Output: Kotlin Programming

// You can also use the name of the companion object
val anotherBook = Book.BookFactory.createBook("Advanced Kotlin")
println(anotherBook.title) // Output: Advanced Kotlin
}

Practical Applications of Companion Objects​

Factory Methods​

One common use of companion objects is to implement factory methods for creating instances of a class:

kotlin
class User private constructor(val name: String, val id: Int) {
companion object {
private var nextId = 1

fun createUser(name: String): User {
return User(name, nextId++)
}

fun createAnonymousUser(): User {
return User("Anonymous", -1)
}
}

override fun toString(): String {
return "User(name='$name', id=$id)"
}
}

fun main() {
val user1 = User.createUser("Alice")
val user2 = User.createUser("Bob")
val anonymous = User.createAnonymousUser()

println(user1) // Output: User(name='Alice', id=1)
println(user2) // Output: User(name='Bob', id=2)
println(anonymous) // Output: User(name='Anonymous', id=-1)
}

Constants and Configuration​

Companion objects are perfect for storing constants and configuration values:

kotlin
class NetworkConfig {
companion object {
const val BASE_URL = "https://api.example.com"
const val TIMEOUT_MS = 5000
const val MAX_RETRIES = 3

fun getFullUrl(endpoint: String): String {
return "$BASE_URL/$endpoint"
}
}
}

fun main() {
println("API Base URL: ${NetworkConfig.BASE_URL}")
println("Full users URL: ${NetworkConfig.getFullUrl("users")}")

// Output:
// API Base URL: https://api.example.com
// Full users URL: https://api.example.com/users
}

Implementing Interfaces​

Companion objects can implement interfaces, which is useful for callback patterns:

kotlin
interface JsonParser {
fun parse(json: String): Map<String, Any>
}

class Product(val name: String, val price: Double) {
companion object : JsonParser {
override fun parse(json: String): Map<String, Any> {
// This is a simplified example, not actual JSON parsing
return mapOf("name" to "Sample Product", "price" to 29.99)
}

fun fromJson(json: String): Product {
val data = parse(json)
return Product(
data["name"] as String,
data["price"] as Double
)
}
}

override fun toString(): String {
return "Product(name='$name', price=$price)"
}
}

fun main() {
val jsonString = """{"name":"Sample Product","price":29.99}"""
val product = Product.fromJson(jsonString)
println(product) // Output: Product(name='Sample Product', price=29.99)
}

Extension Functions on Companion Objects​

You can extend companion objects with extension functions:

kotlin
class StringUtils {
companion object {}
}

// Extension function on the companion object
fun StringUtils.Companion.reverseString(input: String): String {
return input.reversed()
}

fun main() {
val reversed = StringUtils.reverseString("Hello Kotlin")
println(reversed) // Output: niltoK olleH
}

Best Practices and Considerations​

When to Use Companion Objects​

Consider using companion objects when:

  1. You need functionality that is associated with a class rather than its instances
  2. You want to implement the factory pattern
  3. You need to store class-wide constants or configuration
  4. You want to extend class functionality with interfaces

Companion Object vs Object Declaration​

It's important to understand the difference between companion objects and standalone object declarations:

kotlin
// Standalone object declaration
object MathUtils {
fun square(x: Int): Int = x * x
}

class Geometry {
// Companion object within a class
companion object {
fun calculateCircleArea(radius: Double): Double = 3.14 * radius * radius
}
}

fun main() {
// Accessing standalone object
println("Square of 5: ${MathUtils.square(5)}") // Output: Square of 5: 25

// Accessing companion object
println("Circle area: ${Geometry.calculateCircleArea(3.0)}") // Output: Circle area: 28.26
}

The key difference is that companion objects are tied to their containing class, while standalone objects exist independently.

Summary​

Companion objects in Kotlin provide a powerful alternative to Java's static members. They let you:

  • Define constants and utility functions at the class level
  • Implement factory patterns and control object creation
  • Add behavior to classes through interfaces
  • Organize class-level functionality in a clean, object-oriented way

By understanding companion objects, you can write more expressive and maintainable Kotlin code that follows good object-oriented design principles.

Exercises​

  1. Create a Logger class with a companion object that has methods for logging messages at different levels (info, warning, error).

  2. Implement a Database class with a private constructor and a companion object that manages a single connection (singleton pattern).

  3. Create a FileUtils class with a companion object that implements an interface for file operations.

  4. Extend an existing companion object with additional utility methods.

Additional Resources​



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)