Kotlin - Data Classes
Kotlin Data Classes
Data classes are a concise way to create classes that primarily hold data. Kotlin automatically generates useful methods like equals(), hashCode(), toString(), and copy() for you.
Basic Data Class
Simple Data Class Declaration
data class Person(val name: String, val age: Int)
fun main() {
val person = Person("Alice", 30)
println(person) // Person(name=Alice, age=30)
}
Java Equivalent (Much More Verbose)
Kotlin (1 line)
data class Person(
val name: String,
val age: Int
)
Java (30+ lines)
public class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public boolean equals(Object o) {
// ... implementation
}
@Override
public int hashCode() {
// ... implementation
}
@Override
public String toString() {
// ... implementation
}
}
Key Benefit: Data classes eliminate boilerplate code. Kotlin automatically generates equals(), hashCode(), toString(), copy(), and componentN() functions.
Auto-Generated Methods
1. toString() Method
data class User(val name: String, val email: String, val age: Int)
val user = User("Bob", "[email protected]", 25)
println(user) // User(name=Bob, [email protected], age=25)
2. equals() and hashCode() Methods
data class Point(val x: Int, val y: Int)
val point1 = Point(1, 2)
val point2 = Point(1, 2)
val point3 = Point(2, 3)
println(point1 == point2) // true (structural equality)
println(point1 === point2) // false (different objects)
println(point1 == point3) // false (different values)
// hashCode is consistent with equals
val set = setOf(point1, point2, point3)
println(set.size) // 2 (point1 and point2 have same hashCode)
3. copy() Method
data class Customer(val name: String, val email: String, val age: Int)
val originalCustomer = Customer("Alice", "[email protected]", 30)
// Create a copy with some modified properties
val updatedCustomer = originalCustomer.copy(email = "[email protected]")
println(originalCustomer) // Customer(name=Alice, [email protected], age=30)
println(updatedCustomer) // Customer(name=Alice, [email protected], age=30)
// Copy without changes
val duplicateCustomer = originalCustomer.copy()
println(duplicateCustomer == originalCustomer) // true
Destructuring Declarations
Data classes automatically generate componentN() functions for destructuring:
data class Coordinate(val x: Double, val y: Double)
val point = Coordinate(3.0, 4.0)
// Destructuring assignment
val (x, y) = point
println("x = $x, y = $y") // x = 3.0, y = 4.0
// In function parameters
fun calculateDistance(point1: Coordinate, point2: Coordinate): Double {
val (x1, y1) = point1
val (x2, y2) = point2
return kotlin.math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1))
}
// In loops
val points = listOf(
Coordinate(0.0, 0.0),
Coordinate(1.0, 1.0),
Coordinate(2.0, 2.0)
)
for ((x, y) in points) {
println("Point at ($x, $y)")
}
Partial Destructuring
data class Person(val name: String, val age: Int, val city: String)
val person = Person("Alice", 30, "New York")
// Use only some components
val (name, age) = person // city is ignored
val (personName, _, personCity) = person // age is ignored with _
println("$personName from $personCity") // Alice from New York
Data Class Properties
Primary Constructor Properties
// All parameters become properties
data class Book(
val title: String,
val author: String,
val pages: Int,
val isAvailable: Boolean = true // Default value
)
val book = Book("1984", "George Orwell", 328)
println(book.title) // 1984
println(book.isAvailable) // true
var vs val in Data Classes
data class Account(
val id: String, // Immutable - good for identity
var balance: Double, // Mutable - can change
var isActive: Boolean = true
)
val account = Account("ACC001", 1000.0)
// account.id = "ACC002" // ❌ Compilation error
account.balance = 1500.0 // ✅ Allowed
account.isActive = false // ✅ Allowed
Additional Properties (Not in Primary Constructor)
data class Student(val name: String, val grades: List<Int>) {
// Additional properties (not included in generated methods)
val averageGrade: Double
get() = if (grades.isEmpty()) 0.0 else grades.average()
val letterGrade: String
get() = when {
averageGrade >= 90 -> "A"
averageGrade >= 80 -> "B"
averageGrade >= 70 -> "C"
averageGrade >= 60 -> "D"
else -> "F"
}
}
val student = Student("Bob", listOf(85, 92, 78, 88))
println("${student.name}: ${student.letterGrade} (${student.averageGrade})")
Important: Only properties declared in the primary constructor are included in equals(), hashCode(), toString(), and destructuring.
Data Class Requirements
Requirements for Data Classes
- Primary constructor must have at least one parameter
- All primary constructor parameters must be marked as
val
orvar
- Data classes cannot be abstract, open, sealed, or inner
- Data classes can implement interfaces
- Data classes can inherit from other classes (since Kotlin 1.1)
// ✅ Valid data classes
data class Point(val x: Int, val y: Int)
data class Person(val name: String, var age: Int)
// ❌ Invalid data classes
// data class Empty() // No parameters
// data class Invalid(name: String) // Parameter not val/var
// abstract data class Abstract(val x: Int) // Cannot be abstract
// open data class Open(val x: Int) // Cannot be open
Inheritance with Data Classes
Data Classes Implementing Interfaces
interface Drawable {
fun draw()
}
data class Circle(val radius: Double, val color: String) : Drawable {
override fun draw() {
println("Drawing a $color circle with radius $radius")
}
}
data class Rectangle(val width: Double, val height: Double, val color: String) : Drawable {
override fun draw() {
println("Drawing a $color rectangle ${width}x${height}")
}
}
val shapes: List<Drawable> = listOf(
Circle(5.0, "red"),
Rectangle(10.0, 8.0, "blue")
)
shapes.forEach { it.draw() }
Data Classes with Inheritance (Since Kotlin 1.1)
open class Animal(val name: String)
data class Dog(val breed: String, val ownerName: String) : Animal("Dog") {
fun bark() = println("$breed dog barks!")
}
data class Cat(val color: String, val isIndoor: Boolean) : Animal("Cat") {
fun meow() = println("$color cat meows!")
}
val dog = Dog("Golden Retriever", "Alice")
val cat = Cat("Orange", true)
println(dog) // Dog(breed=Golden Retriever, ownerName=Alice)
println(cat) // Cat(color=Orange, isIndoor=true)
Common Use Cases
1. Value Objects
data class Money(val amount: Double, val currency: String) {
init {
require(amount >= 0) { "Amount cannot be negative" }
require(currency.length == 3) { "Currency must be 3 characters" }
}
operator fun plus(other: Money): Money {
require(currency == other.currency) { "Cannot add different currencies" }
return Money(amount + other.amount, currency)
}
}
val price1 = Money(10.50, "USD")
val price2 = Money(5.25, "USD")
val total = price1 + price2
println(total) // Money(amount=15.75, currency=USD)
2. API Response Models
data class ApiResponse<T>(
val success: Boolean,
val data: T?,
val errorMessage: String? = null
)
data class User(
val id: Long,
val username: String,
val email: String,
val createdAt: String
)
// Usage
val response = ApiResponse(
success = true,
data = User(1, "alice", "[email protected]", "2023-01-01")
)
if (response.success && response.data != null) {
println("User: ${response.data.username}")
}
3. Configuration Objects
data class DatabaseConfig(
val host: String = "localhost",
val port: Int = 5432,
val database: String,
val username: String,
val password: String,
val connectionTimeout: Int = 30,
val maxConnections: Int = 10
)
val config = DatabaseConfig(
database = "myapp",
username = "user",
password = "secret"
)
// Easy to create variations
val testConfig = config.copy(
database = "myapp_test",
port = 5433
)
Advanced Patterns
Nested Data Classes
data class Address(
val street: String,
val city: String,
val zipCode: String,
val country: String = "USA"
)
data class Person(
val name: String,
val age: Int,
val address: Address,
val phoneNumbers: List<String> = emptyList()
)
val person = Person(
name = "Alice Johnson",
age = 30,
address = Address("123 Main St", "Springfield", "12345"),
phoneNumbers = listOf("555-1234", "555-5678")
)
// Copying with nested changes
val movedPerson = person.copy(
address = person.address.copy(city = "New Springfield")
)
Data Classes with Validation
data class Email(val value: String) {
init {
require(value.contains("@")) { "Invalid email format" }
require(value.isNotBlank()) { "Email cannot be blank" }
}
}
data class User(
val id: Long,
val name: String,
val email: Email,
val age: Int
) {
init {
require(name.isNotBlank()) { "Name cannot be blank" }
require(age >= 0) { "Age cannot be negative" }
}
}
// Usage with validation
try {
val user = User(1, "Alice", Email("[email protected]"), 30)
println(user)
} catch (e: IllegalArgumentException) {
println("Validation error: ${e.message}")
}
Sealed Data Classes
sealed class Result<out T>
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String, val code: Int = 0) : Result<Nothing>()
object Loading : Result<Nothing>()
fun handleResult(result: Result<String>) {
when (result) {
is Success -> println("Data: ${result.data}")
is Error -> println("Error: ${result.message} (${result.code})")
Loading -> println("Loading...")
}
}
// Usage
handleResult(Success("Hello, World!"))
handleResult(Error("Network timeout", 408))
handleResult(Loading)
Performance Considerations
Memory and Performance Tips
- Immutability: Prefer
val
properties for better thread safety - Copy Performance: copy() creates new instances; avoid in tight loops
- Destructuring: Only extract components you actually need
- Collections: Consider using immutable collections for data class properties
// Good: Immutable data class
data class ImmutableUser(
val id: String,
val name: String,
val preferences: Map<String, String> = emptyMap()
)
// Consider for high-frequency operations
data class Point(val x: Double, val y: Double) {
// Cache expensive calculations
val magnitude: Double by lazy {
kotlin.math.sqrt(x * x + y * y)
}
}
Architecture Note: Data classes are perfect for representing immutable data structures in domain models, DTOs, and value objects. They promote functional programming principles and make code more predictable.
Best Practices
✅ Do
- Use for data containers
- Prefer
val
for immutability - Add validation in init blocks
- Keep them simple and focused
- Use meaningful property names
❌ Don't
- Add complex business logic
- Use for classes with behavior
- Make everything mutable with
var
- Include non-property parameters
- Override generated methods unnecessarily
Practice Exercises
- Create a data class for a book with title, author, pages, and publication year
- Write a data class for a bank account with validation in the init block
- Create nested data classes for a shopping cart with items and customer info
- Implement a data class that represents a coordinate with distance calculation
- Design data classes for a simple blog system (Post, Comment, Author)
Real-World Example: E-commerce System
data class Product(
val id: String,
val name: String,
val price: Double,
val category: String,
val inStock: Boolean = true
) {
init {
require(price >= 0) { "Price cannot be negative" }
require(name.isNotBlank()) { "Product name cannot be blank" }
}
}
data class CartItem(
val product: Product,
val quantity: Int
) {
init {
require(quantity > 0) { "Quantity must be positive" }
}
val totalPrice: Double get() = product.price * quantity
}
data class ShoppingCart(
val items: List<CartItem> = emptyList(),
val customerId: String
) {
val totalAmount: Double get() = items.sumOf { it.totalPrice }
val itemCount: Int get() = items.sumOf { it.quantity }
fun addItem(product: Product, quantity: Int = 1): ShoppingCart {
val newItem = CartItem(product, quantity)
return copy(items = items + newItem)
}
}
// Usage
val laptop = Product("LAPTOP001", "Gaming Laptop", 1299.99, "Electronics")
val mouse = Product("MOUSE001", "Wireless Mouse", 29.99, "Electronics")
val cart = ShoppingCart(customerId = "CUST001")
.addItem(laptop)
.addItem(mouse, 2)
println("Cart total: $${cart.totalAmount}")
println("Items: ${cart.itemCount}")
Quick Quiz
- What methods does Kotlin automatically generate for data classes?
- Can you have a data class with no parameters?
- What's the difference between
==
and===
for data classes? - Which properties are included in the auto-generated methods?
Show answers
- equals(), hashCode(), toString(), copy(), and componentN() for destructuring
- No, data classes must have at least one parameter in the primary constructor
==
checks structural equality (values),===
checks referential equality (same object)- Only properties declared in the primary constructor (marked with val/var)