Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
17: Classes and Constructors
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
SmartDevicethat takes adeviceId(read-only String) and adeviceName(mutable String). - Add a third property,
isConnected(Boolean), which should default tofalsein the primary constructor. - Implement an
initblock that ensures thedeviceIdis not an empty string. Userequire()` to throw an exception if it is empty. - Add a secondary constructor that takes only the
deviceIdanddeviceName, and automatically sets theisConnectedstatus totrue(simulating a device that is registered and immediately online). - In your
mainfunction, instantiate one device using the primary constructor and one using the secondary constructor, then print their properties to verify they were initialized correctly.
There are no comments for now.