Classes & Data Classes
🏗️ Classes & Data Classes
🧱 The Primary Constructor
Kotlin classes declare their constructor parameters directly in the class header, and can turn them into properties with a single keyword:
class Person(val name: String, var age: Int) val p = Person("Alice", 30) println(p.name) // Alice println(p.age) // 30 p.age = 31 // fine — age is a var property // p.name = "Bob" // compile error — name is a val property
val and var inside the constructor parentheses do two things at once: declare the parameter and promote it to a property of the class, automatically generating a getter (and a setter, for var). There's no separate field declaration, no this.name = name assignment, and no hand-written getter/setter — all of that Java ceremony collapses into the single constructor line.
A Simple Class: Java vs Kotlin
| Java | Kotlin |
|---|---|
| public class Person { private final String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } | class Person(val name: String, var age: Int) |
⚙️ Init Blocks & Body Properties
Logic that needs to run at construction time goes in an init block; properties that aren't simple constructor parameters are declared in the class body:
class Person(val name: String, var age: Int) { val isAdult: Boolean init { require(age >= 0) { "Age cannot be negative" } isAdult = age >= 18 println("Created Person: $name") } fun greet() = "Hi, I'm $name" }
require(...) throws an IllegalArgumentException if the condition is false — Kotlin's built-in, one-line way of validating constructor arguments, instead of writing an explicit if and throw.
📋 Data Classes
Adding the data keyword to a class generates equals(), hashCode(), toString(), and copy() automatically, based on the constructor properties:
data class Point(val x: Int, val y: Int) val a = Point(1, 2) val b = Point(1, 2) println(a) // Point(x=1, y=2) — auto-generated toString() println(a == b) // true — auto-generated equals(), compares values not references
toString() — Free Debug Output
A plain Kotlin (or Java) class prints something unhelpful like Point@6d06d69c by default. A data class prints Point(x=1, y=2) — every property, readable, with zero code written.
equals() — Structural, Not Referential
A plain class's == compares references (are these the same object in memory?). A data class's == compares values — two separately-constructed Point(1, 2) instances are equal.
📄 copy()
Data classes also generate a copy() function, which creates a new instance with the same values, except for whichever properties you explicitly override:
data class User(val name: String, val age: Int, val isAdmin: Boolean = false) val original = User("Alice", 30) val birthday = original.copy(age = 31) val promoted = original.copy(isAdmin = true) println(original) // User(name=Alice, age=30, isAdmin=false) — unchanged println(birthday) // User(name=Alice, age=31, isAdmin=false) println(promoted) // User(name=Alice, age=30, isAdmin=true)
This pairs naturally with everything from Chapter 1 — data classes are usually built from val properties, so copy() is the standard way to produce an "updated" version without ever mutating the original. It's the same immutable-update pattern as JavaScript's spread syntax ({ ...user, age: 31 }), but type-checked and built into the class itself.
🏢 Companion Objects
Kotlin has no static keyword. Instead, members that belong to the class itself (not an instance) live inside a companion object:
class User private constructor(val name: String) { companion object { fun guest() = User("Guest") } } val g = User.guest() // called on the class itself, like a Java static method println(g.name) // Guest
"Static" Members: Java vs Kotlin
| Java | Kotlin |
|---|---|
| static User guest() { return new User("Guest"); } | companion object { fun guest() = User("Guest") } |
| User.guest() | User.guest() |
The call syntax looks identical to Java's static call — User.guest() — but under the hood, a companion object is a real singleton object tied to the class, which is why it can also implement interfaces or hold its own properties, something a Java static block can't do.
💻 Coding Challenges
Challenge 1: A Class with an Init Block
Write a class BankAccount(val owner: String, var balance: Double) with an init block that uses require to reject a negative starting balance. Add a function deposit(amount: Double) that increases balance. Create an account, deposit twice, and print the final balance.
Goal: Combine constructor properties, init-block validation, and a regular method.
Challenge 2: Data Class Equality & toString
Declare a data class Book(val title: String, val author: String, val year: Int). Create two separate Book instances with identical values and print whether they're equal with ==. Print one of them directly and observe the auto-generated toString() output.
Goal: See structural equality and free toString() in action.
Challenge 3: copy() and a Companion Object
Using the same Book data class, use copy() to create a second-edition version of a book with an updated year, leaving the original unchanged. Then add a companion object with a function unknown() that returns a placeholder Book("Unknown", "Unknown", 0), and call it via Book.unknown().
Goal: Practice copy() for immutable updates and companion objects for factory-style functions.
Once Android chapters begin later on, data classes become the default shape for API response models, database entities, and UI state objects — anywhere a class exists mainly to hold values. The equals()/toString()/copy() trio this chapter covers isn't a niche feature; it's one of the most-used parts of the whole language.
🎯 What's Next
Next chapter: Inheritance & Interfaces — open/abstract classes, interfaces, polymorphism, and sealed classes.