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}")  
        }  
    }  
}