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.
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.
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 representationequals()
and hashCode()
methods for object comparisoncopy()
method for creating modified copiescomponentN()
functions for destructuring declarationsTo create a valid Kotlin data class, you must follow these essential requirements:
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
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
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)
}
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
}
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")
}
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")
}
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)}")
}
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")
}
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}")
}
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")
}
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")
}
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")
}
}
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)
}
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}")
}
}
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}")
}
}
}