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.