Working with Kotlin Data Classes, Sealed Classes, and Generics

Data Classes

In Kotlin, data classes are specifically designed to hold data. The data keyword marks a class as a data class, automatically generating several useful methods.

data class UserProfile(val identifier: String, val years: Int)

When you declare a data class, the compiler automatically generates the following members from the primary constructor properties:

  • equals() and hashCode() functions
  • toString() method with format like "UserProfile(identifier=John, years=42)"
  • componentN() functions corresponding to each property in declaration order
  • copy() function

Data classes must adhere to these requirements:

  • The primary constructor must have at least one parameter
  • All primary constructor parameters must be marked as val or var
  • Data classes cannot be abstract, open, sealed, or inner

For JVM compatibility, if a no-argument constructor is needed, all properties must have default values:

data class UserProfile(val identifier: String = "", val years: Int = 0)

Properties declared in the class body are not used in the generated functions. To exclude a property from implementations, declare it in the class body:

data class Person(val name: String) {
    var age: Int = 0
}

// Only the name property is used in toString(), equals(), hashCode(), and copy()
val person1 = Person("Alice")
val person2 = Person("Alice")
person1.age = 30
person2.age = 40

The copy() function allows creating a copy of an object with modified properties:

fun copy(identifier: String = this.identifier, years: Int = this.years) = UserProfile(identifier, years)

// Usage:
val user = UserProfile("Bob", 25)
val olderUser = user.copy(years = 30)

The generated component functions enable destructuring declarations:

val profile = UserProfile("Carol", 28)
val (name, age) = profile
println("$name, $age years old") // Outputs "Carol, 28 years old"

The Kotlin standard library provides Pair and Triple classes, but named data classes are generally preferred for better readability.

Sealed Classes

Sealed classes represent restricted class hierarchies when a value can have one of several types, but no others. They extend enums by allowing multiple instances with state.

To declare a sealed class, use the sealed modifier. All subclasses must be declared in the same file as the sealed class:

sealed class Expression
data class Constant(val value: Double) : Expression()
data class Addition(val expr1: Expression, val expr2: Expression) : Expression()
object InvalidExpression : Expression()

Sealed classes are inherently abstract and cannot be instantiated directly. They cannot have non-private constructors (default is private).

The main advantage of sealed classes is their use with when expressions. If you can verify that all cases are covered, an else clause is not required:

fun evaluate(expr: Expression): Double = when(expr) {
    is Constant -> expr.value
    is Addition -> evaluate(expr.expr1) + evaluate(expr.expr2)
    InvalidExpression -> Double.NaN
    // No else clause needed as all cases are covered
}

Generics

Like Java, Kotlin classes can have type parameters:

class Container<T>(item: T) {
    var value = item
}

To create instances of generic classes, provide type parameters:

val box: Container<Int> = Container<Int>(1)

// Type parameters can be omitted when they can be inferred:
val inferredBox = Container(1) // Compiler infers Container<Int>

Variance

Kotlin provides declaration-site variance and type projections instead of Java's wildcard types.

Declaration-site Variance

The out modifier marks a type parameter as covariant - it can only be "produced" and not "consumed":

interface Producer<out T> {
    fun produce(): T
}

fun demo(strs: Producer<String>) {
    val objects: Producer<Any> = strs // OK because T is covariant
}

The in marker indicates contravariance - the type parameter can only be consumed:

interface Consumer<in T> {
    fun consume(item: T)
}

fun demo(x: Consumer<Number>) {
    x.consume(1.0)
    val y: Consumer<Double> = x // OK!
}

Type Projections

Type projections restrict the generic type's usage:

class Array<T>(val size: Int) {
    fun get(index: Int): T { /* ... */ }
    fun set(index: Int, value: T) { /* ... */ }
}

fun copy(from: Array<out Any>, to: Array<Any>) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

Star Projections

When you know nothing about a type parameter but need to use it safely, Kotlin provides star projections:

  • For Foo, Foo<* is equivalent to Foo
  • For Foo, Foo<* is equivalent to
  • For Foo, Foo<* acts as Foo for reading and Foo for writing

Generic Functions

Functions can also have type parameters:

fun <T> createSingleton(item: T): List<T> {
    return listOf(item)
}

fun <T> T.basicToString(): String {
    return toString()
}

// Calling generic functions:
val list = createSingleton<Int>(1)
val inferredList = createSingleton(1) // Type inferred

Type Constraints

Type parameters can be constrained using bounds. The most common constraint is an upper bound:

fun <T : Comparable<T>> sort(items: List<T>) {
    // Implementation
}

sort(listOf(1, 2, 3)) // OK, Int implements Comparable<Int>

For multiple bounds, use a where clause:

fun <T> filterAndConvert(items: List<T>, threshold: T): List<String>
    where T : CharSequence,
          T : Comparable<T> {
    return items.filter { it > threshold }.map { it.toString() }
}

Type Erasure

Kotlin's type safety for generics is enforced only at compile time. At runtime, generic type instances don't retain information about their type parameters. This is called type erasure.

Runtime type checking with generic types isn't possible:

// This won't work at runtime:
// if (foo is List<String>) { ... }

// Unchecked casts are possible but generate warnings:
val foo = ArrayList<Any>()
val strList = foo as List<String> // Warning: unchecked cast

Inline functions, however, can use reified type parameters that preserve type information at runtime:

inline fun <reified T> isInstance(item: Any): Boolean {
    return item is T
}

Tags: kotlin Data Classes Sealed Classes generics Type Safety

Posted on Thu, 27 Aug 2026 16:22:09 +0000 by XzorZ