Primary and Secondary Constructors
A constructor is the code that runs when you create an instance of a class, and Kotlin gives you two flavors: a primary constructor baked directly into the class header, and one or more secondary constructors written as extra constructor(...) blocks inside the class body. Kotlin pushes you toward the primary constructor for the normal case — it is concise, pairs naturally with default parameter values, and eliminates most of the constructor-overloading boilerplate that Java classes accumulate. Secondary constructors still exist for the cases where you genuinely need multiple, differently-shaped ways to build an object, especially when interoperating with Java frameworks or when construction logic can’t be expressed as simple default values. This lesson covers both in depth: how they are declared, how init blocks fit in, the exact order everything runs in, and the mistakes that trip up newcomers.
Overview: How Constructors Work in Kotlin
In Java, a constructor is a special method-like block inside the class body. In Kotlin, the primary constructor is part of the class header itself — the parentheses right after the class name. class Person(val name: String, val age: Int) declares a class and its constructor in one line: no separate field declarations, no assignment statements, no boilerplate. If a constructor parameter is prefixed with val or var, Kotlin automatically turns it into a property of the class, readable (and, for var, writable) from outside. A parameter without val/var is just a plain constructor parameter — it exists only while the object is being built and is not exposed as a property afterward.
The primary constructor cannot contain executable code directly. Instead, any logic that needs to run during construction — validation, logging, computing a derived property — goes inside one or more init blocks. A class can have several init blocks, and the compiler runs them in the exact order they appear in the class body, interleaved with property initializers that also appear in that body. This ordering matters: an init block can see and use primary constructor parameters directly (they are in scope for the whole class body), but it can only see properties that were initialized above it.
Secondary constructors are declared with the constructor keyword inside the class body. If the class has a primary constructor, every secondary constructor must delegate to it — either directly with : this(...) or indirectly through another secondary constructor that eventually reaches the primary one. The compiler enforces this so that the primary constructor’s parameters, property initializers, and init blocks always run first, no matter which constructor the caller used. Only after that delegation completes does the body of the secondary constructor itself run. A class is also allowed to have no primary constructor at all and rely purely on secondary constructors, though this is uncommon in idiomatic Kotlin.
| Aspect | Primary constructor | Secondary constructor |
|---|---|---|
| Where declared | In the class header, e.g. class Foo(val x: Int) |
Inside the class body with the constructor keyword |
| Can hold executable code? | No — use an init block instead |
Yes — the body after the delegation call |
val/var parameters become properties? |
Yes, if marked val/var |
No — secondary constructor parameters are never properties |
| Delegation requirement | None | Must call the primary constructor (directly or indirectly) if one exists |
| Typical use | The normal, everyday way to construct instances | Alternate construction paths, Java interop, framework requirements |
Syntax
The general shape of a class with both constructor types looks like this:
class ClassName(primaryParam1: Type1, val primaryParam2: Type2 = default2) {
// property initializer -- runs in declaration order
val computed: Type3 = primaryParam1.someTransform()
// init block(s) -- run in declaration order, interleaved with
// property initializers above and below them
init {
// has direct access to primary constructor parameters
}
// secondary constructor -- must delegate to the primary constructor
constructor(other: Type4) : this(other.toType1(), default2) {
// additional logic that only applies to this construction path
}
}
primaryParam1— a plain constructor parameter; usable inside the class body but not exposed as a property.val primaryParam2— becomes a read-only property;varwould make it a mutable property instead.= default2— a default value, letting callers omit the argument entirely.init { ... }— executable code that runs as part of construction, in source order relative to property initializers.constructor(...) : this(...)— a secondary constructor; the part after the colon is the mandatory delegation call.
Examples
Example 1: A Basic Primary Constructor
The simplest and most common case — a class whose only job is to hold a couple of values.
class Person(val name: String, val age: Int)
fun main() {
val person = Person("Ava", 30)
println("${person.name} is ${person.age} years old")
}
Output:
Ava is 30 years old
There is no separate field declaration, no assignment statements, and no explicit constructor body — the class header does all of it. Because both parameters are declared with val, person.name and person.age are accessible as read-only properties from outside the class.
Example 2: Init Blocks and Default Parameters
This example computes a derived value in an init block and shows a default parameter in action.
class Rectangle(val width: Double, val height: Double = 1.0) {
val area: Double
init {
area = width * height
println("Created a rectangle of area $area")
}
}
fun main() {
val r1 = Rectangle(4.0, 5.0)
val r2 = Rectangle(3.0)
println("r1 area: ${r1.area}")
println("r2 area: ${r2.area}")
}
Output:
Created a rectangle of area 20.0
Created a rectangle of area 3.0
r1 area: 20.0
r2 area: 3.0
area is declared without an initial value and then assigned inside init — the compiler tracks that this assignment happens exactly once before the object is usable, so it is legal even though area is a val. Because height has a default of 1.0, r2 can be constructed with a single argument. Notice the init block runs immediately as each object is built, so its println fires before either of the later two println calls.
Example 3: Secondary Constructors That Delegate
Here a secondary constructor adds an optional third piece of data on top of the primary constructor’s two required ones.
class Employee(val name: String, val salary: Double) {
var department: String = "Unassigned"
constructor(name: String, salary: Double, department: String) : this(name, salary) {
this.department = department
}
}
fun main() {
val e1 = Employee("Sam", 55000.0)
val e2 = Employee("Riya", 62000.0, "Engineering")
println("${e1.name} works in ${e1.department}")
println("${e2.name} works in ${e2.department}")
}
Output:
Sam works in Unassigned
Riya works in Engineering
The secondary constructor’s : this(name, salary) call runs the primary constructor first (setting name and salary, initializing department to "Unassigned"), and only then does the secondary constructor’s own body run and overwrite department. department is declared with var because, unlike name and salary, it genuinely needs to be reassigned after the object exists.
How It Works Step by Step
To see the delegation chain clearly, consider a class with no primary constructor at all — just two secondary constructors, one delegating to the other.
class Logger {
val tag: String
constructor(tag: String) {
this.tag = tag
println("Logger created with tag $tag")
}
constructor() : this("DEFAULT")
}
fun main() {
val l1 = Logger("Network")
val l2 = Logger()
println("${l1.tag}, ${l2.tag}")
}
Output:
Logger created with tag Network
Logger created with tag DEFAULT
Network, DEFAULT
Logger("Network")calls theconstructor(tag: String)overload directly. It setstagand prints the message.Logger()calls the no-argument constructor. Before its (empty) body can run, Kotlin evaluates its delegation clause,: this("DEFAULT"), which invokesconstructor(tag: String)with"DEFAULT".- That delegated call sets
tag = "DEFAULT"and prints its own message — this is why the secondprintlnappears beforeLogger()‘s own (empty) body finishes. - Only after both objects exist does
mainprint the final combined line.
This is the general rule: whichever constructor you call, Kotlin always walks the delegation chain down to wherever the real initialization work happens before anything else in that constructor’s own body executes.
Common Mistakes
Mistake 1: Forgetting a secondary constructor must delegate
If a class has a primary constructor, every secondary constructor must call it (directly or through another secondary constructor). Skipping this does not compile.
class Point(val x: Int, val y: Int) {
constructor(x: Int) {
// Error: this secondary constructor never delegates
// to the primary constructor -- required whenever a
// primary constructor exists.
println(x)
}
}
The fix is to add the delegation call after the colon:
class Point(val x: Int, val y: Int) {
constructor(x: Int) : this(x, 0)
override fun toString(): String = "($x, $y)"
}
fun main() {
val p1 = Point(3, 4)
val p2 = Point(5)
println(p1)
println(p2)
}
Output:
(3, 4)
(5, 0)
Mistake 2: Forgetting val/var on a constructor parameter
A primary constructor parameter is only turned into a property if it is marked val or var. Without that keyword, it is just a local parameter usable inside the class body, and any attempt to access it from outside is an unresolved reference.
class Coordinate(x: Int, y: Int) {
// x and y here are plain constructor parameters,
// not properties -- they don't exist outside init/the body.
}
fun main() {
val c = Coordinate(1, 2)
println(c.x) // Error: unresolved reference 'x'
}
Adding val makes them real, readable properties:
class Coordinate(val x: Int, val y: Int)
fun main() {
val c = Coordinate(1, 2)
println("(${c.x}, ${c.y})")
}
Output:
(1, 2)
Mistake 3: Reaching for secondary constructors when default parameters would do
Programmers coming from Java often reach for a stack of secondary constructors to simulate overloading, when Kotlin’s default and named parameters already solve this more concisely.
class Message(val text: String, val sender: String) {
constructor(text: String) : this(text, "Unknown")
constructor() : this("", "Unknown")
}
fun main() {
val m1 = Message("Hello", "Ava")
val m2 = Message("Hi")
val m3 = Message()
println("${m1.sender}: ${m1.text}")
println("${m2.sender}: ${m2.text}")
println("${m3.sender}: ${m3.text}")
}
Output:
Ava: Hello
Unknown: Hi
Unknown:
This compiles and works, but it is three constructors’ worth of ceremony for something a single primary constructor with defaults expresses directly:
class Message(val text: String = "", val sender: String = "Unknown")
fun main() {
val m1 = Message("Hello", "Ava")
val m2 = Message("Hi")
val m3 = Message()
println("${m1.sender}: ${m1.text}")
println("${m2.sender}: ${m2.text}")
println("${m3.sender}: ${m3.text}")
}
Output:
Ava: Hello
Unknown: Hi
Unknown:
Same behavior, one constructor. Reserve secondary constructors for cases default parameters genuinely can’t express, such as adapting a Java framework’s required constructor signature or building from an incompatible source type.
Best Practices
- Prefer the primary constructor with default and named parameters over secondary constructors whenever the alternate “shapes” can be expressed as optional values.
- Mark primary constructor parameters
valby default; only usevarwhen the property genuinely needs to change after construction. - Keep
initblocks focused on validation or derived-value computation; avoid scattering many smallinitblocks that make the initialization order hard to follow. - Remember a
valconstructor parameter is a read-only reference, not deep immutability — avalholding aMutableListcan still have its contents changed. - Use secondary constructors mainly for Java interop or when a class needs to be built from an incompatible source without duplicating primary-constructor logic.
- Validate constructor arguments early with
require()orcheck()inside aninitblock so invalid objects can never be constructed in the first place.
Practice Exercises
- Write a
data class Circle(val radius: Double)with aninitblock that computes and storesarea(Math.PI * radius * radius). Print the area for a circle of radius2.0. - Write a class
Book(val title: String, val author: String, val year: Int = 2024)and add a secondary constructorBook(title: String)that delegates to the primary constructor using"Unknown"as the author. Construct one book with all three arguments and one with just a title, then print both. - Write a class
Temperaturewith no primary constructor and two secondary constructors: one that takes a CelsiusDoubledirectly, and one that takes a FahrenheitDoubleand converts it to Celsius before delegating to the first. Print the stored Celsius value for both construction paths.
Summary
- The primary constructor lives in the class header;
val/varparameters there become properties, plain parameters do not. - Primary constructors cannot contain code directly — use one or more
initblocks, which run in the order they appear, interleaved with property initializers. - Secondary constructors are declared with the
constructorkeyword inside the class body and, if a primary constructor exists, must delegate to it directly or indirectly with: this(...). - Delegation always runs before a secondary constructor’s own body, guaranteeing the primary constructor’s setup always happens first.
- Default parameter values usually replace the need for multiple secondary constructors and are the more idiomatic Kotlin choice.
