Classes & Data Classes

Kotlin Fundamentals — Classes & Data Classes
Kotlin Fundamentals
Course 1 · Chapter 4 · Classes & Data Classes

🏗️ Classes & Data Classes

Kotlin classes cut down dramatically on the boilerplate Java requires for even a simple class — no separate field declarations, no manually-written getters/setters, no repetitive constructor assignment. This chapter covers the primary constructor, properties, and the biggest boilerplate-killer of all: data classes, which generate an entire class's worth of standard methods from a single line.

🧱 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

JavaKotlin
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

JavaKotlin
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.

→ Solution

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.

→ Solution

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.

→ Solution

💡 Data Classes Are Everywhere in Real Kotlin Code

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.