A Kotlin class's primary constructor initializes properties, but sometimes you need to execute arbitrary logic during construction — validate inputs, log creation, register listeners, or set up state that depends on multiple properties. Kotlin provides two mechanisms for this: initializer blocks and secondary constructors.
The Construction Problem
A primary constructor is excellent at initializing properties, but it cannot contain arbitrary code — it exists only as a parameter list in the class header. When you need code to run during construction, you need a separate mechanism.
Class Construction Order
------------------------
1. Primary constructor parameters are evaluated
2. Properties are initialized (in order they appear)
3. init blocks run (in order they appear)
4. Secondary constructor body runs (if used)
Initializer Blocks
An initializer block is a block of code prefixed with the init keyword that runs as part of the primary constructor's execution. It runs every time an instance of the class is created, regardless of which constructor was used.
class Person(val name: String, var weightLbs: Double) {
init {
println("Person created: $name, $weightLbs lbs")
require(weightLbs > 0) { "Weight must be positive" }
}
var weightKilos: Double
get() = weightLbs / 2.2
set(value) { weightLbs = value * 2.2 }
}
When Person("Alice", 140.0) is called, the init block executes immediately after the properties name and weightLbs are assigned.
Key Characteristics of Initializer Blocks
- Always runs during construction — every instance creation triggers all init blocks
- Can have multiple init blocks — you can declare more than one in the same class
- All init blocks run every time — if you have three init blocks, all three run for every instance
- Can access constructor parameters —
nameandweightLbsare available insideinit - Runs in declaration order — if you have multiple init blocks, they execute top to bottom
class MultiInit(val x: Int) {
init {
println("Init block 1: x = $x") // Runs first
}
val doubled = x * 2 // Property initialized between init blocks
init {
println("Init block 2: doubled = $doubled") // Runs second
}
}
val obj = MultiInit(5)
// Output:
// Init block 1: x = 5
// Init block 2: doubled = 10
The second init block can use doubled because property initialization and init blocks run in the order they appear in the class body.
Common Uses for Init Blocks
- Input validation — check constructor arguments and throw exceptions early
- Logging — record that an object was created, for debugging
- Initialization that depends on multiple properties — when the logic requires properties that are initialized separately
- Side effects — registering the object with a registry, incrementing a counter, etc.
class DatabaseConnection(val host: String, val port: Int) {
init {
require(port in 1..65535) { "Port must be between 1 and 65535" }
require(host.isNotBlank()) { "Host must not be blank" }
println("Connecting to $host:$port")
}
}
Secondary Constructors
A secondary constructor is an additional constructor that provides an alternative way to create instances of the class. It is declared inside the class body using the constructor keyword.
class Person(val name: String, var weightLbs: Double) {
var weightKilos: Double
get() = weightLbs / 2.2
set(value) { weightLbs = value * 2.2 }
// Secondary constructor: create a Person by specifying weight in kilos
constructor(name: String, weightKilos: Double, unit: String) : this(name, weightKilos * 2.2) {
println("Created $name from kilos: $weightKilos kg")
}
}
Key Characteristics of Secondary Constructors
- Runs only when used — unlike
initblocks, a secondary constructor only runs if the caller invokes it - Can have multiple secondary constructors — each must have a distinct parameter signature
- Must delegate to the primary constructor — if the class has a primary constructor, every secondary constructor must call it using
this(...)(directly or indirectly) - Code runs after all initializer blocks —
initblocks always run before the secondary constructor body
When secondary constructor is called:
1. Primary constructor parameters evaluated
2. Properties initialized
3. All init blocks run (in order)
4. Secondary constructor body runs
Delegation to Primary Constructor
The : this(...) syntax is mandatory when a primary constructor exists. A secondary constructor cannot bypass the primary constructor — it must route through it.
class Config(val host: String, val port: Int) {
init {
println("Config init: $host:$port")
}
// Secondary constructor delegates to primary, then adds its own logic
constructor(url: String) : this(
host = url.substringBefore(":"),
port = url.substringAfter(":").toInt()
) {
// This body runs AFTER init block
println("Parsed from URL: $url")
}
}
val c = Config("localhost:8080")
// Output:
// Config init: localhost:8080
// Parsed from URL: localhost:8080
Init Blocks vs Secondary Constructors
Choosing between them is a matter of intent:
| Feature | Init Block | Secondary Constructor |
|---|---|---|
| When it runs | Always, on every instance creation | Only when that specific constructor is called |
| Can have multiple | Yes | Yes |
| Runs after primary? | As part of primary | After all init blocks |
| Access to constructor params | Yes | Via delegation |
| Best for | Validation, logging, setup for all instances | Alternative construction paths |
When to Use Init Blocks
Use init when you need logic that every instance requires, regardless of how it was constructed:
- Validating that constructor arguments meet preconditions
- Logging or tracing object creation for debugging
- Registering the object with a shared registry or listener list
When to Use Secondary Constructors
Use secondary constructors when you need alternative ways to construct an object:
- Creating an object from a different representation (e.g., URL string vs host+port)
- Providing convenience constructors for common use cases
- Handling legacy API compatibility where callers may provide different parameter sets
In practice, default parameter values in the primary constructor eliminate many situations where secondary constructors would otherwise be needed:
// Without secondary constructor — using default parameters
class HttpRequest(
val url: String,
val method: String = "GET",
val timeout: Int = 30_000
)
// All of these work:
HttpRequest("https://example.com")
HttpRequest("https://example.com", "POST")
HttpRequest("https://example.com", "POST", 60_000)
HttpRequest(url = "https://example.com", timeout = 5_000)
Default parameters are usually cleaner and more flexible than secondary constructors for optional parameters. Reserve secondary constructors for cases where the construction logic genuinely differs in a meaningful way.
A Complete Example
class NetworkClient(val baseUrl: String, val timeout: Int = 5000) {
private val headers = mutableMapOf<String, String>()
init {
require(baseUrl.startsWith("http")) {
"Base URL must start with http or https"
}
println("NetworkClient created for: $baseUrl")
}
init {
// Second init block — runs after the first
headers["Accept"] = "application/json"
headers["User-Agent"] = "MyApp/1.0"
}
// Secondary constructor for pre-authenticated clients
constructor(baseUrl: String, authToken: String) : this(baseUrl) {
// This runs after both init blocks
headers["Authorization"] = "Bearer $authToken"
println("Client configured with auth token")
}
fun get(path: String): String {
return "GET $baseUrl$path with timeout=$timeout"
}
}
// Using primary constructor
val client = NetworkClient("https://api.example.com")
// Output:
// NetworkClient created for: https://api.example.com
// (both init blocks run, headers populated)
// Using secondary constructor
val authClient = NetworkClient("https://api.example.com", "token123")
// Output:
// NetworkClient created for: https://api.example.com
// (both init blocks run)
// Client configured with auth token
Summary
| Concept | Purpose |
|---|---|
init block | Code that runs for every instance, as part of primary construction |
Multiple init blocks | Allowed; all run in declaration order |
| Secondary constructor | Alternative construction path, declared with constructor keyword |
: this(...) delegation | Required — secondary constructors must call the primary constructor |
| Execution order | Properties → init blocks → secondary constructor body |
Understanding init blocks and secondary constructors gives you full control over what happens when your objects are created. Use init blocks for universal setup and validation, use secondary constructors for genuine alternative construction paths, and use default parameters to handle most cases where you might otherwise reach for a secondary constructor.
Construction logic should be predictable. Know exactly what runs, when, and in what order.