Skip to content
Data Classes and Sealed Classes
step 1/7

Reading — step 1 of 7

Learn

~3 min readClasses and Inheritance

Kotlin's data class and sealed class are language-level shortcuts that replace large chunks of boilerplate-heavy Java code.

data class — for value-type data

data class User(val name: String, val age: Int)

val ada = User("Ada", 36)
val ada2 = User("Ada", 36)

ada == ada2          // true — equals based on properties
ada.toString()       // "User(name=Ada, age=36)"
ada.hashCode()       // consistent with equals

val older = ada.copy(age = 37)   // copy with one field changed

The compiler GENERATES:

  • equals() — structural equality based on all primary-constructor properties
  • hashCode() — matching
  • toString() — readable representation
  • copy(...) — clone with optional field overrides
  • componentN() — for destructuring

Destructuring works automatically:

val (name, age) = ada
println("$name is $age")

When to use data classes:

  • DTOs / model objects
  • Tuples that need names (Pair / Triple are limited)
  • Immutable value containers

Restrictions:

  • Primary constructor must have at least one parameter (all val/var)
  • Cannot be abstract, open, sealed, or inner
  • Inheritance is restricted (no extending another data class)

sealed class — closed hierarchy

A sealed class restricts subclasses to those declared in the same file (or, in newer Kotlin, the same module):

sealed class Result<out T> {
    data class Success<T>(val value: T) : Result<T>()
    data class Failure(val error: String) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

fun <T> handle(r: Result<T>): String = when (r) {
    is Result.Success -> "got ${r.value}"
    is Result.Failure -> "error: ${r.error}"
    Result.Loading -> "still loading"
}
// No `else` needed — compiler knows sealed has only those subclasses

Why sealed?

  • Exhaustive when checks — compiler errors if you miss a case
  • Compiler-known type hierarchy (better optimization, smarter type inference)
  • Refactor-safe — add a new subclass and every when lights up

This is Kotlin's equivalent of Swift enums with associated values, or Rust enum variants.

sealed interface (Kotlin 1.5+)

Like sealed class but as an interface — can be implemented by classes or objects, AND a class can implement multiple sealed interfaces:

sealed interface JsonValue

data class JsonString(val value: String) : JsonValue
data class JsonNumber(val value: Double) : JsonValue
object JsonNull : JsonValue

object — singletons

Declare a one-and-only-one instance:

object Settings {
    var theme: String = "dark"
    fun reset() { theme = "dark" }
}

Settings.theme = "light"
Settings.reset()

No class definition, no getInstance() boilerplate. Thread-safe lazy initialization handled by the compiler.

Companion objects — class-level statics:

class User(val id: Int, val name: String) {
    companion object {
        fun fromJson(json: String): User {
            // parse and return User
            return User(0, "")
        }
    }
}

val u = User.fromJson("...")    // call without an instance

Companion object methods serve as the equivalent of Java static methods.

Common mistakes

  • Using data class for everything — when behavior is the focus, use a regular class. Data class is for value-equality data containers.
  • Forgetting data class when you need equals/hashCode — regular classes use referential equality. Hidden bug source.
  • Adding fields outside the primary constructor in a data class — those fields aren't part of equals/hashCode/copy.
  • Sealed without exhaustive when — if you use else in a when on sealed, the compiler can't help when you add a new subclass.
  • Confusing object expression with object declarationobject : Listener { ... } is an anonymous inner-class-equivalent; object Foo is a singleton.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…