DEV Community

Cover image for Kotlin Classes: A Comprehensive Guide
Elozino Ovedhe
Elozino Ovedhe

Posted on

Kotlin Classes: A Comprehensive Guide

Introduction

Classes are one of the fundamental building blocks of object-oriented programming in Kotlin. They allow you to encapsulate data and behavior into reusable, organized structures. Kotlin's approach to classes is modern and pragmatic, offering several variations to suit different programming needs.

What Are Kotlin Classes?

A Kotlin class is a blueprint for creating objects that combine properties (data) and functions (behavior) into a single unit. Classes enable you to model real-world entities and abstract concepts in your code, making it more organized, maintainable, and reusable.

Basic Class Syntax

class Person {
    var name: String = ""
    var age: Int = 0

    fun greet() {
        println("Hello, my name is $name")
    }
}
Enter fullscreen mode Exit fullscreen mode

Kotlin Classes: A Comprehensive Guide

Introduction

Classes are one of the fundamental building blocks of object-oriented programming in Kotlin. They allow you to encapsulate data and behavior into reusable, organized structures. Kotlin's approach to classes is modern and pragmatic, offering several variations to suit different programming needs.

What Are Kotlin Classes?

A Kotlin class is a blueprint for creating objects that combine properties (data) and functions (behavior) into a single unit. Classes enable you to model real-world entities and abstract concepts in your code, making it more organized, maintainable, and reusable.

Basic Class Syntax

class Person {
    var name: String = ""
    var age: Int = 0

    fun greet() {
        println("Hello, my name is $name")
    }
}
Enter fullscreen mode Exit fullscreen mode

Types of Kotlin Classes

1. Regular Classes

Regular classes are the standard way to define a class in Kotlin. They can have properties, methods, constructors, and inheritance.

Characteristics:

  • Can be instantiated multiple times
  • Support inheritance
  • Can have mutable and immutable properties
  • Full flexibility in implementation

Example:

class Car(val brand: String, var speed: Int = 0) {
    fun accelerate() {
        speed += 10
    }

    fun displayInfo() {
        println("Brand: $brand, Speed: $speed")
    }
}

val myCar = Car("Toyota", 50)
myCar.accelerate()
myCar.displayInfo()
Enter fullscreen mode Exit fullscreen mode

Use Cases:

  • Creating entities that need to maintain state
  • Building complex objects with multiple responsibilities
  • Implementing inheritance hierarchies
  • General-purpose object modeling

2. Data Classes

Data classes are optimized for holding data. Kotlin automatically generates useful methods like equals(), hashCode(), toString(), and copy().

Characteristics:

  • Automatically generate equals(), hashCode(), and toString()
  • Provide a copy() function for creating modified copies
  • Implement componentN() functions for destructuring
  • Require at least one property in the primary constructor
  • Properties must be declared with val or var

Example:

data class User(val id: Int, val name: String, val email: String)

val user1 = User(1, "Alice", "alice@example.com")
val user2 = user1.copy(name = "Bob")

println(user1)  // User(id=1, name=Alice, email=alice@example.com)
println(user1 == user2)  // false (different names)

val (id, name, email) = user1  // Destructuring
Enter fullscreen mode Exit fullscreen mode

Use Cases:

  • Representing data transfer objects (DTOs)
  • Creating immutable data containers
  • Working with API responses
  • Database models
  • Configuration objects

3. Sealed Classes

Sealed classes restrict which classes can inherit from them. They're useful for representing restricted class hierarchies.

Characteristics:

  • All subclasses must be defined in the same file (or same package in Kotlin 1.0)
  • Cannot be instantiated directly
  • Provide exhaustive when expressions
  • Useful for type-safe hierarchies

Example:

sealed class Result {
    data class Success(val data: String) : Result()
    data class Error(val exception: Exception) : Result()
    object Loading : Result()
}

fun handleResult(result: Result) {
    when (result) {
        is Result.Success -> println("Success: ${result.data}")
        is Result.Error -> println("Error: ${result.exception.message}")
        is Result.Loading -> println("Loading...")
    }
}
Enter fullscreen mode Exit fullscreen mode

Use Cases:

  • Representing algebraic data types
  • Creating type-safe result wrappers
  • Handling different states in state machines
  • API response modeling with known variants
  • Pattern matching scenarios

4. Enum Classes

Enum classes represent a fixed set of constants. Each enum constant is an instance of the enum class.

Characteristics:

  • Define a fixed set of values
  • Each constant is an object of the enum type
  • Can have properties and methods
  • Support when expressions naturally
  • Implement Comparable by default

Example:

enum class Direction(val angle: Int) {
    NORTH(0),
    EAST(90),
    SOUTH(180),
    WEST(270);

    fun opposite(): Direction = when (this) {
        NORTH -> SOUTH
        EAST -> WEST
        SOUTH -> NORTH
        WEST -> EAST
    }
}

val direction = Direction.NORTH
println(direction.angle)  // 0
println(direction.opposite())  // SOUTH
Enter fullscreen mode Exit fullscreen mode

Use Cases:

  • Representing fixed sets of options
  • State management with predefined states
  • Direction, color, or priority constants
  • Configuration options
  • Type-safe alternatives to string constants

5. Object Declarations (Singletons)

Object declarations create a singleton; a class with only one instance that exists for the lifetime of the application.

Characteristics:

  • Thread-safe singleton implementation
  • Lazy initialization
  • Only one instance exists
  • No constructor parameters
  • Can inherit from classes and implement interfaces

Example:

object DatabaseConnection {
    private val connection = "Connected to DB"

    fun query(sql: String): String {
        return "Executing: $sql on $connection"
    }
}

println(DatabaseConnection.query("SELECT * FROM users"))
Enter fullscreen mode Exit fullscreen mode

Use Cases:

  • Database connections
  • Logger instances
  • Configuration managers
  • Utility functions
  • Shared resources

6. Companion Objects

Companion objects allow you to define members that belong to the class itself rather than to instances.

Characteristics:

  • One per class
  • Members are accessed like static members in Java
  • Can implement interfaces
  • Can have a name (though usually anonymous)

Example:

class MyClass {
    companion object {
        const val CONSTANT = "Hello"

        fun create(): MyClass = MyClass()
    }
}

println(MyClass.CONSTANT)
val instance = MyClass.create()
Enter fullscreen mode Exit fullscreen mode

Use Cases:

  • Factory methods
  • Constants related to the class
  • Shared utility functions
  • Static-like behavior

7. Inner Classes

Inner classes are nested classes that have access to the outer class's members.

Characteristics:

  • Can access outer class members
  • Require an instance of the outer class
  • Increase memory usage due to implicit reference
  • Use inner keyword

Example:

class Outer {
    private val name = "Outer"

    inner class Inner {
        fun display() {
            println("Accessing outer: $name")
        }
    }
}

val outer = Outer()
val inner = outer.Inner()
inner.display()
Enter fullscreen mode Exit fullscreen mode

Use Cases:

  • Callbacks and event handlers
  • Nested UI components
  • Logical grouping of related classes
  • Access to outer class state

Key Differences Summary

Feature Regular Data Sealed Enum Object Companion
Instantiation Multiple Multiple Via subclasses Fixed constants Single N/A
Auto Methods No Yes No No No No
Inheritance Yes Yes Restricted No Yes N/A
Constructor Yes Yes Yes Yes No No
Use Case General Data holding Type-safe hierarchies Constants Singletons Static-like

Best Practices

  1. Use data classes for objects primarily holding data
  2. Use sealed classes when you have a restricted set of subclasses
  3. Use enums for fixed sets of constants
  4. Use objects for singletons and utility functions
  5. Prefer immutability by using val instead of var
  6. Use companion objects instead of static members
  7. Keep classes focused with a single responsibility

Conclusion

Kotlin provides multiple class types, each optimized for specific scenarios. Understanding when to use each type leads to cleaner, more maintainable code. Regular classes offer flexibility, data classes simplify data handling, sealed classes provide type safety, enums represent constants, and objects/companions handle singleton and static-like patterns. Choose the right tool for your specific use case to write idiomatic Kotlin code.

Kotlin provides multiple class types, each optimized for specific scenarios. Understanding when to use each type leads to cleaner, more maintainable code. Regular classes offer flexibility, data classes simplify data handling, sealed classes provide type safety, enums represent constants, and objects/companions handle singleton and static-like patterns. Choose the right tool for your specific use case to write idiomatic Kotlin code.

Types of Kotlin Classes

1. Regular Classes

Regular classes are the standard way to define a class in Kotlin. They can have properties, methods, constructors, and inheritance.

Characteristics:

  • Can be instantiated multiple times
  • Support inheritance
  • Can have mutable and immutable properties
  • Full flexibility in implementation

Example:

class Car(val brand: String, var speed: Int = 0) {
    fun accelerate() {
        speed += 10
    }

    fun displayInfo() {
        println("Brand: $brand, Speed: $speed")
    }
}

val myCar = Car("Toyota", 50)
myCar.accelerate()
myCar.displayInfo()
Enter fullscreen mode Exit fullscreen mode

Use Cases:

  • Creating entities that need to maintain state
  • Building complex objects with multiple responsibilities
  • Implementing inheritance hierarchies
  • General-purpose object modeling

2. Data Classes

Data classes are optimized for holding data. Kotlin automatically generates useful methods like equals(), hashCode(), toString(), and copy().

Characteristics:

  • Automatically generate equals(), hashCode(), and toString()
  • Provide a copy() function for creating modified copies
  • Implement componentN() functions for destructuring
  • Require at least one property in the primary constructor
  • Properties must be declared with val or var

Example:

data class User(val id: Int, val name: String, val email: String)

val user1 = User(1, "Alice", "alice@example.com")
val user2 = user1.copy(name = "Bob")

println(user1)  // User(id=1, name=Alice, email=alice@example.com)
println(user1 == user2)  // false (different names)

val (id, name, email) = user1  // Destructuring
Enter fullscreen mode Exit fullscreen mode

Use Cases:

  • Representing data transfer objects (DTOs)
  • Creating immutable data containers (For holding states)
  • Working with API responses
  • Database models
  • Configuration objects

3. Sealed Classes

Sealed classes restrict which classes can inherit from them. They're useful for representing restricted class hierarchies.

Characteristics:

  • All subclasses must be defined in the same file (or same package in Kotlin 1.0)
  • Cannot be instantiated directly
  • Provide exhaustive when expressions
  • Useful for type-safe hierarchies

Example:

sealed class Result {
    data class Success(val data: String) : Result()
    data class Error(val exception: Exception) : Result()
    object Loading : Result()
}

fun handleResult(result: Result) {
    when (result) {
        is Result.Success -> println("Success: ${result.data}")
        is Result.Error -> println("Error: ${result.exception.message}")
        is Result.Loading -> println("Loading...")
    }
}
Enter fullscreen mode Exit fullscreen mode

Use Cases:

  • Representing algebraic data types
  • Creating type-safe result wrappers
  • Handling different states in state machines
  • API response modeling with known variants
  • Pattern matching scenarios

4. Enum Classes

Enum classes represent a fixed set of constants. Each enum constant is an instance of the enum class.

Characteristics:

  • Define a fixed set of values
  • Each constant is an object of the enum type
  • Can have properties and methods
  • Support when expressions naturally
  • Implement Comparable by default

Example:

enum class Direction(val angle: Int) {
    NORTH(0),
    EAST(90),
    SOUTH(180),
    WEST(270);

    fun opposite(): Direction = when (this) {
        NORTH -> SOUTH
        EAST -> WEST
        SOUTH -> NORTH
        WEST -> EAST
    }
}

val direction = Direction.NORTH
println(direction.angle)  // 0
println(direction.opposite())  // SOUTH
Enter fullscreen mode Exit fullscreen mode

Use Cases:

  • Representing fixed sets of options
  • State management with predefined states
  • Direction, color, or priority constants
  • Configuration options
  • Type-safe alternatives to string constants

5. Object Declarations (Singletons)

Object declarations create a singleton; a class with only one instance that exists for the lifetime of the application.

Characteristics:

  • Thread-safe singleton implementation
  • Lazy initialization
  • Only one instance exists
  • No constructor parameters
  • Can inherit from classes and implement interfaces

Example:

object DatabaseConnection {
    private val connection = "Connected to DB"

    fun query(sql: String): String {
        return "Executing: $sql on $connection"
    }
}

println(DatabaseConnection.query("SELECT * FROM users"))
Enter fullscreen mode Exit fullscreen mode

Use Cases:

  • Database connections
  • Logger instances
  • Configuration managers
  • Utility functions
  • Shared resources

6. Companion Objects

Companion objects allow you to define members that belong to the class itself rather than to instances.

Characteristics:

  • One per class
  • Members are accessed like static members in Java
  • Can implement interfaces
  • Can have a name (though usually anonymous)

Example:

class MyClass {
    companion object {
        const val CONSTANT = "Hello"

        fun create(): MyClass = MyClass()
    }
}

println(MyClass.CONSTANT)
val instance = MyClass.create()
Enter fullscreen mode Exit fullscreen mode

Use Cases:

  • Factory methods
  • Constants related to the class
  • Shared utility functions
  • Static-like behavior

7. Inner Classes

Inner classes are nested classes that have access to the outer class's members.

Characteristics:

  • Can access outer class members
  • Require an instance of the outer class
  • Increase memory usage due to implicit reference
  • Use inner keyword

Example:

class Outer {
    private val name = "Outer"

    inner class Inner {
        fun display() {
            println("Accessing outer: $name")
        }
    }
}

val outer = Outer()
val inner = outer.Inner()
inner.display()
Enter fullscreen mode Exit fullscreen mode

Use Cases:

  • Callbacks and event handlers
  • Nested UI components
  • Logical grouping of related classes
  • Access to outer class state

8. Abstract Classes

An abstract class in Kotlin is a class that cannot be instantiated directly. Instead, it serves as a blueprint or template that other classes must inherit from and implement.

Characteristics:

  • Cannot be instantiated: You can't create an object directly from an abstract class; you must create a subclass that extends it.
  • Can contain abstract members: Abstract classes can have abstract properties and functions that subclasses must implement:
  • Can have concrete members: Abstract classes can also have regular properties and functions with implementations (like sleep() above), which subclasses inherit automatically.
  • Enforces a contract: Abstract classes ensure that all subclasses implement required functionality, making your code more structured and predictable.

Example:

abstract class Animal {
    abstract val name: String  // Subclasses must provide this
    abstract fun makeSound()   // Subclasses must implement this

    fun sleep() {              // Regular function (optional to override)
        println("$name is sleeping")
    }
}

class Dog(override val name: String) : Animal() {
    override fun makeSound() {
        println("Woof!")
    }
}

val dog = Dog("Buddy")
dog.makeSound()  // Woof!
dog.sleep()      // Buddy is sleeping
Enter fullscreen mode Exit fullscreen mode

Use cases:

  • When you want to define common behavior for related classes
  • When you need to enforce that subclasses implement certain methods
  • When you want to share code among closely related classes

Key Differences Summary

Feature Regular Data Sealed Enum Object Companion Abstract
Instantiation Multiple Multiple Via subclasses Fixed constants Single N/A Via subclasses
Auto Methods No Yes No No No No No
Inheritance Yes Yes Restricted No Yes N/A Yes
Constructor Yes Yes Yes Yes No No Yes
Use Case General Data holding Type-safe hierarchies Constants Singletons Static-like Define blueprints

Best Practices

  1. Use data classes for objects primarily holding data
  2. Use sealed classes when you have a restricted set of subclasses
  3. Use enums for fixed sets of constants
  4. Use objects for singletons and utility functions
  5. Prefer immutability by using val instead of var
  6. Use companion objects instead of static members
  7. Keep classes focused with a single responsibility

Conclusion

Kotlin provides multiple class types, each optimized for specific scenarios. Understanding when to use each type leads to cleaner, more maintainable code. Regular classes offer flexibility, data classes simplify data handling, sealed classes provide type safety, enums represent constants, and objects/companions handle singleton and static-like patterns. Choose the right tool for your specific use case to write idiomatic Kotlin code.

Kotlin provides multiple class types, each optimized for specific scenarios. Understanding when to use each type leads to cleaner, more maintainable code. Regular classes offer flexibility, data classes simplify data handling, sealed classes provide type safety, enums represent constants, and objects/companions handle singleton and static-like patterns. Choose the right tool for your specific use case to write idiomatic Kotlin code.

Top comments (0)