Kotlin Data Class

When working with Kotlin development, Kotlin data class represents one of the most powerful features for creating clean, efficient code. A Kotlin data class automatically generates essential methods like toString(), equals(), hashCode(), and copy(), making it perfect for holding data structures in your applications. Whether you’re building Android apps or backend services, understanding Kotlin data class fundamentals will significantly improve your code quality and reduce boilerplate.

What is a Kotlin Data Class?

A Kotlin data class is a special type of class designed primarily to hold data. Unlike regular classes, a Kotlin data class automatically provides implementations for common object methods, eliminating the need to write repetitive boilerplate code. The Kotlin data class is declared using the data keyword before the class declaration.

Basic Kotlin Data Class Syntax

data class Student(val name: String, val age: Int, val grade: Double)

In this Kotlin data class example, the compiler automatically generates:

  • toString() method for readable string representation
  • equals() and hashCode() methods for object comparison
  • copy() method for creating modified copies
  • componentN() functions for destructuring declarations

Kotlin Data Class Requirements

To create a valid Kotlin data class, you must follow these essential requirements:

Primary Constructor Parameters

The Kotlin data class primary constructor must have at least one parameter. All parameters must be marked as val (read-only) or var (mutable):

// Valid Kotlin data class
data class Product(val id: Long, var name: String, val price: Double)

// Invalid - no parameters
// data class Empty() // Compilation error

Class Modifiers Restrictions

A Kotlin data class cannot be:

  • abstract
  • open
  • sealed
  • inner
// Valid Kotlin data class
data class BookInfo(val title: String, val author: String)

// Invalid examples
// abstract data class AbstractBook() // Error
// open data class OpenBook() // Error

Essential Kotlin Data Class Properties

Automatic toString() Generation

Every Kotlin data class automatically generates a toString() method that displays all primary constructor properties:

data class Vehicle(val brand: String, val model: String, val year: Int)

fun demonstrateToString() {
    val car = Vehicle("Toyota", "Camry", 2023)
    println(car.toString()) // Vehicle(brand=Toyota, model=Camry, year=2023)
}

Automatic equals() and hashCode() Generation

Kotlin data class instances support structural equality comparison through automatically generated equals() and hashCode() methods:

data class Employee(val id: Int, val name: String, val department: String)

fun demonstrateEquality() {
    val emp1 = Employee(101, "Alice Johnson", "Engineering")
    val emp2 = Employee(101, "Alice Johnson", "Engineering")
    val emp3 = Employee(102, "Bob Smith", "Marketing")
    
    println(emp1 == emp2) // true - same content
    println(emp1 == emp3) // false - different content
    println(emp1.hashCode() == emp2.hashCode()) // true
}

Copy Function in Kotlin Data Class

The copy() function allows creating new Kotlin data class instances with modified properties while keeping others unchanged:

data class GameCharacter(val name: String, val level: Int, val health: Int, val mana: Int)

fun demonstrateCopy() {
    val wizard = GameCharacter("Gandalf", 50, 100, 200)
    
    // Create copy with modified level and health
    val leveledWizard = wizard.copy(level = 55, health = 120)
    
    println("Original: $wizard")
    println("Leveled: $leveledWizard")
    
    // Only change one property
    val healedWizard = wizard.copy(health = 150)
    println("Healed: $healedWizard")
}

Destructuring Declarations with Kotlin Data Class

Kotlin data class supports destructuring declarations through automatically generated componentN() functions, allowing you to unpack objects into separate variables:

data class Coordinate(val x: Double, val y: Double, val z: Double)

fun demonstrateDestructuring() {
    val point = Coordinate(10.5, 20.3, 5.7)
    
    // Destructure all components
    val (xPos, yPos, zPos) = point
    println("X: $xPos, Y: $yPos, Z: $zPos")
    
    // Destructure only some components
    val (x, y) = point
    println("2D Position: ($x, $y)")
    
    // Skip components using underscore
    val (_, _, z) = point
    println("Z coordinate only: $z")
}

Destructuring in Function Parameters

Kotlin data class destructuring works seamlessly with function parameters:

data class Rectangle(val width: Double, val height: Double)

fun calculateArea(rect: Rectangle): Double {
    val (w, h) = rect
    return w * h
}

fun calculatePerimeter(rect: Rectangle): Double {
    val (width, height) = rect
    return 2 * (width + height)
}

fun demonstrateParameterDestructuring() {
    val rectangle = Rectangle(15.0, 10.0)
    
    println("Area: ${calculateArea(rectangle)}")
    println("Perimeter: ${calculatePerimeter(rectangle)}")
}

Advanced Kotlin Data Class Features

Data Class with Default Values

Kotlin data class properties can have default values, making object creation more flexible:

data class UserProfile(
    val username: String,
    val email: String,
    val isActive: Boolean = true,
    val score: Int = 0,
    val registrationDate: String = "2025-01-01"
)

fun demonstrateDefaults() {
    // Create with all parameters
    val user1 = UserProfile("alice_dev", "alice@example.com", true, 1500, "2025-01-15")
    
    // Create with some defaults
    val user2 = UserProfile("bob_user", "bob@example.com")
    
    // Create with named parameters
    val user3 = UserProfile(
        username = "charlie_pro", 
        email = "charlie@example.com", 
        score = 2500
    )
    
    println("User1: $user1")
    println("User2: $user2")
    println("User3: $user3")
}

Data Class Inheritance

While Kotlin data class cannot be open, they can extend other classes and implement interfaces:

interface Identifiable {
    val id: String
}

open class BaseEntity(open val createdAt: String)

data class DatabaseRecord(
    override val id: String,
    val data: String,
    override val createdAt: String
) : BaseEntity(createdAt), Identifiable

fun demonstrateInheritance() {
    val record = DatabaseRecord("REC001", "Sample data", "2025-07-07")
    println("Record: $record")
    println("ID: ${record.id}")
    println("Created: ${record.createdAt}")
}

Nested Data Classes

Kotlin data class can contain other data classes as properties:

data class Address(val street: String, val city: String, val zipCode: String)
data class Contact(val phone: String, val email: String)
data class Person(val name: String, val age: Int, val address: Address, val contact: Contact)

fun demonstrateNested() {
    val address = Address("123 Main St", "Springfield", "12345")
    val contact = Contact("+1-555-0123", "john.doe@email.com")
    val person = Person("John Doe", 30, address, contact)
    
    println("Person: $person")
    
    // Access nested properties
    println("City: ${person.address.city}")
    println("Phone: ${person.contact.phone}")
    
    // Copy with modified nested data
    val newAddress = address.copy(street = "456 Oak Ave")
    val movedPerson = person.copy(address = newAddress)
    println("Moved person: $movedPerson")
}

Working with Collections and Kotlin Data Class

Kotlin data class objects work excellently with collections due to their proper equals() and hashCode() implementations:

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

fun demonstrateCollections() {
    val library = listOf(
        Book("978-1234567890", "Kotlin in Action", "Dmitry Jemerov", 360),
        Book("978-0987654321", "Android Development", "Jane Smith", 450),
        Book("978-1111111111", "Clean Code", "Robert Martin", 464),
        Book("978-2222222222", "Design Patterns", "Gang of Four", 395)
    )
    
    // Filter books by page count
    val thickBooks = library.filter { it.pages > 400 }
    println("Books with more than 400 pages:")
    thickBooks.forEach { println("  ${it.title} - ${it.pages} pages") }
    
    // Group books by author
    val booksByAuthor = library.groupBy { it.author }
    println("\nBooks grouped by author:")
    booksByAuthor.forEach { (author, books) ->
        println("  $author: ${books.map { it.title }}")
    }
    
    // Find specific book
    val searchIsbn = "978-1234567890"
    val foundBook = library.find { it.isbn == searchIsbn }
    println("\nFound book: $foundBook")
}

Map Operations with Kotlin Data Class

data class Student(val id: Int, val name: String, val grade: Double)

fun demonstrateMapOperations() {
    val students = listOf(
        Student(1, "Emma Wilson", 92.5),
        Student(2, "Michael Brown", 88.0),
        Student(3, "Sarah Davis", 95.2),
        Student(4, "James Miller", 87.8)
    )
    
    // Transform to map
    val studentMap = students.associateBy { it.id }
    println("Student map: $studentMap")
    
    // Calculate statistics
    val averageGrade = students.map { it.grade }.average()
    println("Average grade: $averageGrade")
    
    // Find top performer
    val topStudent = students.maxByOrNull { it.grade }
    println("Top student: $topStudent")
    
    // Destructuring in forEach
    students.forEach { (id, name, grade) ->
        println("Student $id: $name scored $grade")
    }
}

Best Practices for Kotlin Data Class

Use Immutable Properties

Prefer val over var in Kotlin data class for thread safety and predictable behavior:

// Recommended: Immutable data class
data class ImmutableOrder(val orderId: String, val amount: Double, val timestamp: Long)

// Use copy() for modifications
fun processOrder(order: ImmutableOrder): ImmutableOrder {
    val processedAmount = order.amount * 1.1 // Add 10% processing fee
    return order.copy(amount = processedAmount)
}

Validate Data in Init Block

Add validation logic using init blocks in Kotlin data class:

data class ValidatedUser(val username: String, val email: String, val age: Int) {
    init {
        require(username.isNotBlank()) { "Username cannot be blank" }
        require(email.contains("@")) { "Invalid email format" }
        require(age >= 0) { "Age cannot be negative" }
    }
}

fun demonstrateValidation() {
    try {
        val validUser = ValidatedUser("alice123", "alice@example.com", 25)
        println("Valid user created: $validUser")
        
        // This will throw an exception
        val invalidUser = ValidatedUser("", "invalid-email", -5)
    } catch (e: IllegalArgumentException) {
        println("Validation error: ${e.message}")
    }
}

Complete Example: Building a Library Management System

Here’s a comprehensive example demonstrating Kotlin data class usage in a real-world scenario:

import java.time.LocalDate
import java.time.format.DateTimeFormatter

// Data classes for library system
data class Author(val id: Int, val name: String, val nationality: String)

data class Book(
    val isbn: String,
    val title: String,
    val authors: List<Author>,
    val publicationYear: Int,
    val genre: String,
    val pageCount: Int,
    val isAvailable: Boolean = true
)

data class Member(
    val memberId: String,
    val name: String,
    val email: String,
    val joinDate: LocalDate,
    val membershipType: String = "REGULAR"
)

data class BorrowRecord(
    val recordId: String,
    val book: Book,
    val member: Member,
    val borrowDate: LocalDate,
    val dueDate: LocalDate,
    val returnDate: LocalDate? = null
) {
    val isOverdue: Boolean
        get() = returnDate == null && LocalDate.now().isAfter(dueDate)
    
    val isReturned: Boolean
        get() = returnDate != null
}

class LibrarySystem {
    private val books = mutableListOf<Book>()
    private val members = mutableListOf<Member>()
    private val borrowRecords = mutableListOf<BorrowRecord>()
    
    fun addBook(book: Book) {
        books.add(book)
        println("Added book: ${book.title}")
    }
    
    fun addMember(member: Member) {
        members.add(member)
        println("Added member: ${member.name}")
    }
    
    fun borrowBook(isbn: String, memberId: String): BorrowRecord? {
        val book = books.find { it.isbn == isbn && it.isAvailable }
        val member = members.find { it.memberId == memberId }
        
        if (book != null && member != null) {
            val borrowDate = LocalDate.now()
            val dueDate = borrowDate.plusWeeks(2)
            val recordId = "BR${System.currentTimeMillis()}"
            
            val borrowRecord = BorrowRecord(recordId, book, member, borrowDate, dueDate)
            borrowRecords.add(borrowRecord)
            
            // Update book availability
            val updatedBook = book.copy(isAvailable = false)
            books.removeIf { it.isbn == isbn }
            books.add(updatedBook)
            
            println("Book borrowed successfully!")
            return borrowRecord
        }
        
        println("Unable to borrow book. Check availability and member ID.")
        return null
    }
    
    fun returnBook(recordId: String): Boolean {
        val record = borrowRecords.find { it.recordId == recordId && !it.isReturned }
        
        if (record != null) {
            val returnDate = LocalDate.now()
            val updatedRecord = record.copy(returnDate = returnDate)
            
            borrowRecords.removeIf { it.recordId == recordId }
            borrowRecords.add(updatedRecord)
            
            // Update book availability
            val book = record.book
            val availableBook = book.copy(isAvailable = true)
            books.removeIf { it.isbn == book.isbn }
            books.add(availableBook)
            
            println("Book returned successfully!")
            return true
        }
        
        println("Invalid record ID or book already returned.")
        return false
    }
    
    fun getOverdueBooks(): List<BorrowRecord> {
        return borrowRecords.filter { it.isOverdue }
    }
    
    fun getMemberBorrowHistory(memberId: String): List<BorrowRecord> {
        return borrowRecords.filter { it.member.memberId == memberId }
    }
    
    fun searchBooksByGenre(genre: String): List<Book> {
        return books.filter { it.genre.equals(genre, ignoreCase = true) }
    }
    
    fun getPopularAuthors(): Map<Author, Int> {
        return borrowRecords
            .flatMap { it.book.authors }
            .groupingBy { it }
            .eachCount()
    }
}

fun main() {
    val library = LibrarySystem()
    
    // Create authors
    val author1 = Author(1, "J.K. Rowling", "British")
    val author2 = Author(2, "George Orwell", "British")
    val author3 = Author(3, "Agatha Christie", "British")
    
    // Create books
    val book1 = Book(
        isbn = "978-0-439-70818-6",
        title = "Harry Potter and the Philosopher's Stone",
        authors = listOf(author1),
        publicationYear = 1997,
        genre = "Fantasy",
        pageCount = 309
    )
    
    val book2 = Book(
        isbn = "978-0-452-28423-4",
        title = "1984",
        authors = listOf(author2),
        publicationYear = 1949,
        genre = "Dystopian Fiction",
        pageCount = 328
    )
    
    val book3 = Book(
        isbn = "978-0-06-207348-4",
        title = "Murder on the Orient Express",
        authors = listOf(author3),
        publicationYear = 1934,
        genre = "Mystery",
        pageCount = 256
    )
    
    // Add books to library
    library.addBook(book1)
    library.addBook(book2)
    library.addBook(book3)
    
    // Create members
    val member1 = Member(
        memberId = "M001",
        name = "Alice Johnson",
        email = "alice.johnson@email.com",
        joinDate = LocalDate.of(2024, 1, 15),
        membershipType = "PREMIUM"
    )
    
    val member2 = Member(
        memberId = "M002",
        name = "Bob Smith",
        email = "bob.smith@email.com",
        joinDate = LocalDate.of(2024, 3, 20)
    )
    
    // Add members to library
    library.addMember(member1)
    library.addMember(member2)
    
    // Demonstrate borrowing
    println("\n--- Borrowing Books ---")
    val borrowRecord1 = library.borrowBook("978-0-439-70818-6", "M001")
    val borrowRecord2 = library.borrowBook("978-0-452-28423-4", "M002")
    
    // Show borrow records using destructuring
    borrowRecord1?.let { (recordId, book, member, borrowDate, dueDate) ->
        println("Record: $recordId")
        println("Book: ${book.title}")
        println("Member: ${member.name}")
        println("Borrowed: $borrowDate, Due: $dueDate")
    }
    
    // Demonstrate searching
    println("\n--- Searching Books ---")
    val fantasyBooks = library.searchBooksByGenre("Fantasy")
    fantasyBooks.forEach { book ->
        val (isbn, title, authors, year, genre, pages) = book
        println("$title by ${authors.map { it.name }.joinToString(", ")} ($year)")
    }
    
    // Demonstrate member history
    println("\n--- Member History ---")
    val aliceHistory = library.getMemberBorrowHistory("M001")
    aliceHistory.forEach { record ->
        println("${record.book.title} - Borrowed: ${record.borrowDate}")
    }
    
    // Return a book
    println("\n--- Returning Books ---")
    borrowRecord1?.let { library.returnBook(it.recordId) }
    
    // Check overdue books
    println("\n--- Overdue Books ---")
    val overdueBooks = library.getOverdueBooks()
    if (overdueBooks.isEmpty()) {
        println("No overdue books!")
    } else {
        overdueBooks.forEach { record ->
            println("${record.book.title} - Due: ${record.dueDate}, Member: ${record.member.name}")
        }
    }
}