Skip to Content
Course content

17: Classes and Constructors

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you're coming from Java or C#, your instinct is probably to treat a class like a container: you declare your fields at the top, and then you write a constructor to shuffle those values into the fields. In Kotlin, if you do this, you're fighting the language. I see this all the time with developers transitioning to Kotlin—they write "Java in Kotlin syntax," and it results in a lot of noise that doesn't need to be there.

The Boilerplate Trap

Let's look at a naive implementation of a GameCharacter. Suppose we want every character to have a name and a starting health value. A developer stuck in the old way of thinking might write something like this:

class GameCharacter(name: String, health: Int) {
    var characterName: String
    var characterHealth: Int

    init {
        this.characterName = name
        this.characterHealth = health
    }
}

Now, does this work? Yes. Is it a nightmare to maintain? Also yes. Look at how many times we've mentioned name and health. We define them as parameters in the constructor, then we declare them as properties in the class body, and then we manually assign them inside an init block. It's repetitive, and in a real-world project with twenty properties per class, your files will be twice as long as they need to be.

Letting the Primary Constructor do the Heavy Lifting

Kotlin allows us to collapse all of that ceremony into a single line. By adding val or var directly to the constructor parameters, you aren't just defining a local variable for the constructor to use—you are telling Kotlin to declare a property for the class and assign the value automatically.

class GameCharacter(val name: String, var health: Int)

That's it. That's the whole class. I love this because it forces you to think about the intent of the data. Use val if the property should be read-only (immutable) and var if it needs to change during the game. By moving the declarations into the primary constructor, we've eliminated the redundant init block and the separate property declarations entirely.

Handling Edge Cases with Secondary Constructors

Of course, real life is rarely that simple. What happens when you want a "default" character? Maybe most characters start with 100 health, and you don't want to pass that number every single time you instantiate the class. You could use default arguments in the primary constructor—which is usually the best move—but sometimes you need a completely different way to build your object.

This is where secondary constructors come in. They allow you to provide alternative paths for creation, provided they all eventually call the primary constructor using the this() keyword. I've found that secondary constructors are most useful when you're dealing with legacy APIs or when the logic for creating a "special" version of a class is too complex for a simple default value.

class GameCharacter(val name: String, var health: Int) {
    // A secondary constructor for "Default" characters
    constructor(name: String) : this(name, 100) {
        println("Creating a standard character with default health.")
    }
}

One thing to keep in mind: don't overdo it. If you find yourself writing five different secondary constructors, you're probably better off using a Factory pattern or a companion object. Keep your constructors focused on initialization, not complex business logic.

When the 'init' Block Actually Matters

You might be wondering if the init block is entirely useless now. Not at all. The init block is for logic that cannot be handled by a simple assignment. If you need to validate that the health isn't negative, or if you need to trigger a network call the moment an object is born, that's where init shines.

class GameCharacter(val name: String, var health: Int) {
    init {
        require(health > 0) { "A character cannot start with $health health; they'd be dead on arrival!" }
    }
}

By combining a concise primary constructor with a targeted init block, you get the best of both worlds: minimal boilerplate and robust validation.




📋 Practical Task

Exercise: Implementing a SmartHome Device Registry

You are building a system to manage smart home devices. Your goal is to create a SmartDevice class that demonstrates a clean use of Kotlin constructors.

  • Create a primary constructor for SmartDevice that takes a deviceId (read-only String) and a deviceName (mutable String).
  • Add a third property, isConnected (Boolean), which should default to false in the primary constructor.
  • Implement an init block that ensures the deviceId is not an empty string. Use require()` to throw an exception if it is empty.
  • Add a secondary constructor that takes only the deviceId and deviceName, and automatically sets the isConnected status to true (simulating a device that is registered and immediately online).
  • In your main function, instantiate one device using the primary constructor and one using the secondary constructor, then print their properties to verify they were initialized correctly.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.