Skip to content

Kotlin Data Classes — Complete Guide with Examples

DodaTech Updated 2026-06-28 10 min read

In this tutorial, you will learn about Kotlin Data Classes. We cover key concepts, practical examples, and best practices to help you master this topic.

Kotlin data classes are classes marked with the data keyword that automatically generate equals, hashCode, toString, copy, componentN functions, and destructuring declarations from all properties declared in the primary constructor.

What You'll Learn

  • Declare data classes with minimal syntax
  • Understand auto-generated functions
  • Use the copy function for immutable updates
  • Apply destructuring declarations
  • Override auto-generated functions when needed
  • Follow data class constraints and rules
  • Work with data classes in collections

Why It Matters

Data classes eliminate an enormous amount of boilerplate. In Java, a model class with fields, getters, setters, equals, hashCode, toString, and a constructor takes 50-80 lines. In Kotlin, it takes one line. Data classes are the default choice for modeling data in Kotlin applications. They are essential for Android development, API response models, and any code that deals with structured data.

Real-World Use

DodaTech uses data classes for all API response models, database entities, and event objects. The auto-generated copy function enables immutable update patterns that prevent accidental state mutation. Destructuring makes it easy to extract fields from complex response objects in the malware signature processing pipeline.

Learning Path

flowchart LR
  A[Interfaces] --> B[Data Classes\nYou are here]
  B --> C[Objects]
  style B fill:#f90,color:#fff

Basic Data Class Syntax

Add the data keyword before class to enable automatic function generation.

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

fun main() {
    val user = User(1, "Alice", "alice@example.com")
    
    // toString
    println(user)  // Output: User(id=1, name=Alice, email=alice@example.com)
    
    // equals and hashCode
    val same = User(1, "Alice", "alice@example.com")
    println(user == same)   // Output: true (structural equality)
    println(user === same)  // Output: false (different references)
    
    // copy
    val updated = user.copy(email = "alice@newdomain.com")
    println(updated)  // Output: User(id=1, name=Alice, email=alice@newdomain.com)
    
    // Component functions (1-based)
    println(user.component1())  // Output: 1
    println(user.component2())  // Output: Alice
    println(user.component3())  // Output: alice@example.com
}

Output: The data keyword generates toString with all properties, structural equals, hashCode, a copy function with named arguments, and componentN functions.

Destructuring Declarations

Data classes support destructuring declarations, assigning properties to variables in one line.

data class Point(val x: Double, val y: Double)
data class Rectangle(val topLeft: Point, val bottomRight: Point, val label: String = "")

fun main() {
    val point = Point(3.0, 4.0)
    val (x, y) = point
    println("x=$x, y=$y")  // Output: x=3.0, y=4.0
    
    // Destructuring in loops
    val points = listOf(Point(1.0, 2.0), Point(3.0, 4.0))
    for ((px, py) in points) {
        println("($px, $py)")
    }
    // Output: (1.0, 2.0) / (3.0, 4.0)
    
    // Destructuring nested data classes
    val rect = Rectangle(Point(0.0, 0.0), Point(10.0, 10.0), "Square")
    val (topLeft, bottomRight) = rect
    println("$topLeft to $bottomRight")
    // Output: Point(x=0.0, y=0.0) to Point(x=10.0, y=10.0)
    
    // Ignoring properties with underscore
    val (_, _, label) = rect
    println("Label: $label")  // Output: Label: Square
}

Output: Destructuring assigns each property to a variable in declaration order. Use _ to skip properties you do not need.

Copy Function

The copy function creates a new instance with modified properties while keeping the rest unchanged.

data class Configuration(
    val host: String,
    val port: Int,
    val useTls: Boolean,
    val timeoutSeconds: Int,
    val maxRetries: Int
)

fun main() {
    val default = Configuration(
        host = "localhost",
        port = 8080,
        useTls = false,
        timeoutSeconds = 30,
        maxRetries = 3
    )
    
    // Immutable update pattern
    val production = default.copy(
        host = "api.example.com",
        useTls = true
    )
    
    val staging = default.copy(
        host = "staging.example.com",
        port = 9090,
        timeoutSeconds = 60
    )
    
    println(default.host)  // Output: localhost
    println(production.host)  // Output: api.example.com
    println(staging.host)     // Output: staging.example.com
    
    // Specific overrides
    val debug = default.copy(
        timeoutSeconds = 300,
        maxRetries = 0
    )
    println("${debug.host}:${debug.port} (tls=${debug.useTls})")
    // Output: localhost:8080 (tls=false)
}

Output: The copy function preserves all properties not explicitly changed. This enables immutable updates without deep cloning.

Auto-Generated Function Details

The compiler generates the following functions for data classes:

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

// Generates approximately:
// equals(other: Any?): Boolean - compares all primary constructor properties
// hashCode(): Int - hash based on all primary constructor properties
// toString(): String - "Person(name=..., age=...)"
// copy(name: String = this.name, age: Int = this.age): Person
// component1(): String = name
// component2(): Int = age

All functions use only properties from the primary constructor. Properties declared in the class body are excluded.

data class Employee(val id: Int, val name: String) {
    var department: String = "Unknown"  // Excluded from equals/hashCode/toString
    val uppercaseName: String get() = name.uppercase()  // Computed, excluded
}

fun main() {
    val emp1 = Employee(1, "Alice").apply { department = "Engineering" }
    val emp2 = Employee(1, "Alice").apply { department = "Sales" }
    
    println(emp1 == emp2)  // Output: true (department is excluded)
    println(emp1)  // Output: Employee(id=1, name=Alice)
}

Output: Properties in the class body are not part of the auto-generated functions. Only primary constructor properties are included.

Overriding Auto-Generated Functions

You can override any auto-generated function if you need custom behavior.

data class Product(val id: Int, val name: String, val price: Double) {
    // Custom toString: include formatted price
    override fun toString(): String {
        return "Product #$id: $name ($${String.format("%.2f", price)})"
    }
    
    // Custom equals: compare by ID only
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Product) return false
        return id == other.id
    }
    
    override fun hashCode(): Int = id
}

fun main() {
    val p1 = Product(1, "Laptop", 999.99)
    val p2 = Product(1, "Laptop Pro", 1299.99)
    
    println(p1)        // Output: Product #1: Laptop ($999.99)
    println(p1 == p2)  // Output: true (same ID)
    println(p1 === p2) // Output: false (different objects)
}

Output: Custom overrides can change the behavior of equality, string representation, or hashing as needed.

Data Classes in Collections

Data classes work naturally with collections due to structural equality.

data class Book(val isbn: String, val title: String, val author: String)

fun main() {
    val books = listOf(
        Book("978-1234", "Kotlin in Action", "Dmitry Jemerov"),
        Book("978-5678", "Effective Kotlin", "Marcin Moskala"),
        Book("978-9012", "Kotlin Coroutines", "Marcin Moskala")
    )
    
    // Remove duplicates using Set (requires proper equals/hashCode)
    val withDuplicates = books + Book("978-1234", "Kotlin in Action", "Dmitry Jemerov")
    println(withDuplicates.size)  // Output: 4
    val unique = withDuplicates.toSet()
    println(unique.size)  // Output: 3
    
    // Find by value
    val target = Book("978-5678", "Effective Kotlin", "Marcin Moskala")
    println(books.contains(target))  // Output: true
    
    // Group by author
    val byAuthor = books.groupBy { it.author }
    println(byAuthor.keys)  // Output: [Dmitry Jemerov, Marcin Moskala]
    
    // Map to different representation
    val titles = books.map { it.title }
    println(titles)  // Output: [Kotlin in Action, Effective Kotlin, Kotlin Coroutines]
}

Output: Data classes work correctly with collections because equals and hashCode are based on property values.

Data Class Constraints

Data classes have specific constraints.

// Must have at least one property in primary constructor
// Cannot be open, abstract, sealed, or inner
// Cannot extend another class (but can implement interfaces)

interface Validatable {
    fun validate(): Boolean
}

data class ValidatedUser(
    val username: String,
    val password: String
) : Validatable {
    override fun validate(): Boolean {
        return username.length >= 3 && password.length >= 8
    }
}

fun main() {
    val user = ValidatedUser("alice", "secret123")
    println(user.validate())  // Output: true
    
    // Can have secondary constructors (rarely needed)
    data class Color(val red: Int, val green: Int, val blue: Int) {
        constructor(hex: Int) : this(
            (hex shr 16) and 0xFF,
            (hex shr 8) and 0xFF,
            hex and 0xFF
        )
    }
    
    val red = Color(0xFF0000)
    println(red)  // Output: Color(red=255, green=0, blue=0)
}

Output: Data classes can implement interfaces, have secondary constructors, and can be used as value objects with validation.

Common Mistakes

  1. Including mutable collections in data classes: A data class with a mutable list field produces a hashCode that changes when the list is modified. This breaks hash-based collections. Use immutable collections or exclude the field from equals/hashCode.

  2. Using data classes for JPA entities: JPA proxies break equals and hashCode in data classes. Use regular classes with custom equals based on the ID field for JPA entities.

  3. Forgetting that body properties are excluded: Properties not in the primary constructor are excluded from auto-generated functions. If you need them in equals, include them in the constructor.

  4. Using inheritance with data classes: Data classes cannot inherit from other classes (interfaces only). Use Composition Over Inheritance for Data Modeling.

  5. Assuming copy does a deep copy: copy only does shallow copy. If a property is a reference type, the copied data class shares the same reference.

  6. Destructuring with wrong property order: Destructuring follows declaration order. Reordering constructor parameters silently breaks destructuring callers.

Practice Questions

  1. What functions does the data keyword generate automatically?

Answer: equals, hashCode, toString, copy, componentN (one per property), and destructuring declarations.

  1. How do you create a modified copy of a data class?

Answer: Use the copy function with named arguments for the properties to change: user.copy(email = "new@example.com").

  1. Can a data class extend another class?

Answer: No. Data classes cannot extend other classes. They can only implement interfaces.

  1. What happens to mutable collections in data classes?

Answer: They are included in equals and hashCode if declared in the primary constructor. Mutating them changes the hash code, breaking behavior in hash-based collections.

  1. Challenge: Create an Order data class with LineItem sub-data classes. Implement a totalPrice property that computes the total. Use copy for immutable updates and destructuring in a reporting function.

Answer:

data class LineItem(val productName: String, val quantity: Int, val unitPrice: Double)

data class Order(val id: Long, val items: List<LineItem>, val discount: Double = 0.0) {
    val totalPrice: Double
        get() {
            val subtotal = items.sumOf { it.quantity * it.unitPrice }
            return subtotal * (1 - discount)
        }
}

fun printOrderSummary(order: Order) {
    val (id, items, discount) = order
    println("Order #$id")
    println("Items (${items.size}):")
    for ((name, qty, price) in items) {
        println("  $name: $qty x $price = ${qty * price}")
    }
    println("Discount: ${discount * 100}%")
    println("Total: ${order.totalPrice}")
}

fun main() {
    val order = Order(
        1001,
        listOf(
            LineItem("Kotlin Book", 2, 49.99),
            LineItem("USB Cable", 3, 9.99)
        ),
        0.1
    )
    
    printOrderSummary(order)
    
    // Immutable update: add another item
    val updated = order.copy(
        items = order.items + LineItem("Mouse", 1, 25.00)
    )
    println("\nUpdated total: ${updated.totalPrice}")
}

Mini Project

Build a shopping cart system using data classes. Requirements:

  • CartItem data class with productId, name, quantity, unitPrice
  • ShoppingCart data class with items list and computed total
  • CartOperations interface with add, remove, updateQuantity, and clear
  • Implement CartOperations with data class usage
  • Use copy for immutable updates
  • Implement a checkout function that creates an immutable order snapshot
  • Write tests verifying immutability and structural equality

This project demonstrates data classes, immutability, destructuring, and copy in a practical e-commerce scenario.

FAQ

Can I exclude a property from generated functions?

Yes. If the property is not in the primary constructor, it is excluded. For primary constructor properties, you cannot exclude them. Use a regular class if you need selective inclusion.

How many component functions does a data class generate?

One component function per primary constructor property, up to 22 properties. Access them as .component1() through .componentN().

Can I use data classes with inheritance hierarchies?

Data classes cannot extend other classes. Use an interface for the common contract and have multiple data classes implement it.

Does copy perform a shallow or deep copy?

Shallow copy. Reference-type properties are shared between the original and the copy. For deep copies, implement custom copy logic.

Are data classes the same as value objects?

Data classes are a convenient way to create value objects, but they are not strictly value objects because the copy function allows creating different instances with different property values.

What's Next

After mastering data classes, learn about objects for singletons, companion objects, and anonymous classes. You can also explore sealed classes for restricted class hierarchies.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro