Kotlin Classes, Objects and Constructors
Kotlin 14 min read
The primary constructor is part of the class header, init blocks and property initialisers run in declaration order, and a secondary constructor must delegate before it can do anything.
A Kotlin class declaration carries its constructor in the header, which removes most of the
boilerplate a Java class needs and introduces one ordering rule worth knowing precisely: property
initialisers and init blocks run interleaved, in the order they appear in the source, and all
of them run before any secondary constructor body.
Written against Kotlin 1.9.
The primary constructor
class User(val id: Long, var name: String, private val email: String)
One line declares a class, three properties, a constructor, getters, a setter for name, and, with
nothing more, nothing else. val and var in the header make the parameters into properties;
omitting the keyword makes them constructor parameters that are not stored:
class User(name: String) {
val displayName = name.uppercase() // name is a parameter, used and discarded
}
That distinction matters for memory: a parameter without val does not become a field, so a large
object passed only to derive something smaller is not retained.
The constructor keyword is required only when the constructor needs annotations or a visibility
modifier:
class User private constructor(val id: Long)
class Service @Inject constructor(private val repo: Repository)
init blocks and initialisation order
The primary constructor has no body. Code that must run at construction goes in an init block:
class User(val email: String) {
val domain: String
init {
require(email.contains("@")) { "Invalid email: $email" }
domain = email.substringAfter("@")
}
}
require throws IllegalArgumentException and is the idiomatic argument check. check throws
IllegalStateException for a state precondition, and both take a lazily evaluated message lambda —
the string is only built when the check fails.
The ordering rule:
class Ordered(val a: String) {
val first = log("property 1")
init { log("init 1") }
val second = log("property 2")
init { log("init 2") }
}
// property 1, init 1, property 2, init 2
Initialisers and init blocks are interleaved in source order, not “all properties then all
inits”. That is why a property can be read from an init block only if it is declared above it:
class Broken {
init {
println(value.length) // compile error — value is declared below
}
val value = "text"
}
The compiler catches that case. It does not catch the equivalent through a function call, which is the next section.
Never call an open member from a constructor
open class Base {
open val label: String = "base"
init {
println(label.length) // NullPointerException when constructing Derived
}
}
class Derived : Base() {
override val label: String = "derived"
}
The base class is fully initialised before the subclass’s property initialisers run, so at the moment
Base.init executes, Derived.label has not been assigned. The overridden property is null
despite being declared non-nullable, and the compiler issues only a warning.
If the base needs a value from the subclass, take it as a constructor parameter:
open class Base(val label: String) {
init { println(label.length) } // safe — passed in
}
class Derived : Base("derived")
Secondary constructors
class User(val id: Long, val name: String) {
var email: String? = null
constructor(id: Long, name: String, email: String) : this(id, name) {
this.email = email
}
}
A secondary constructor must delegate to the primary with : this(...), directly or through another
secondary. Everything in the primary, property initialisers and every init block, runs before
the secondary’s body.
Secondary constructors are rarely needed, because default arguments usually replace them:
class User(val id: Long, val name: String, var email: String? = null)
One declaration instead of two, and it composes with named arguments. Reach for a secondary constructor when the alternative construction path needs different logic, not merely different defaults.
When a class has no primary constructor, a secondary one delegates to the superclass directly with
: super(...) and there is no init ordering to reason about.
object, for a singleton
object Config {
val timeout = Duration.ofSeconds(30)
fun reload() { }
}
Config.timeout
object declares a class and its single instance at once. It is initialised lazily and thread-safely
on first access, the JVM’s class-initialisation lock does the work, so this is a correct singleton
with no double-checked locking.
It cannot take constructor parameters, which is the constraint that decides most uses: anything
needing configuration cannot be an object.
An object is the right shape for a stateless helper or a genuine constant holder. It is the wrong
shape for anything with mutable state, because a global singleton with state is a global variable —
untestable and shared across every caller.
companion object
class User private constructor(val id: Long, val name: String) {
companion object {
private const val MAX_NAME = 50
fun of(id: Long, name: String): User {
require(name.length <= MAX_NAME)
return User(id, name.trim())
}
}
}
User.of(1, "Ada")
Kotlin has no static. A companion object is a single object tied to the class, and its members
are called through the class name.
Two details. It is not static in the bytecode, members are instance methods on a Companion
class, so Java callers write User.Companion.of(...) unless the member is annotated @JvmStatic.
And const val inside it is a real static field, inlined at every call site, which is why constants
belong there rather than as ordinary vals.
A private constructor plus a companion factory is the idiomatic way to validate before construction, since a constructor cannot return an alternative type or a cached instance.
Nested and inner classes
class Outer {
private val value = 1
class Nested {
// no access to Outer's members
}
inner class Inner {
fun read() = value // can reach the outer instance
}
}
Outer.Nested()
Outer().Inner()
A nested class does not hold a reference to the outer instance, the opposite of Java, where a
non-static inner class does by default. Adding inner opts into that reference.
The default here is the safer one, and it is a deliberate correction of Java’s: an unintended outer reference is how a short-lived object keeps a large one alive, which is a memory leak that no code inspection makes obvious.
Data classes and value classes
data class Point(val x: Int, val y: Int)
@JvmInline
value class UserId(val value: Long)
A data class generates equals, hashCode, toString, copy and
destructuring from the primary constructor’s properties.
A value class, previously called an inline class, wraps a single value with no allocation at
runtime: UserId compiles to a bare
Long wherever the compiler can manage it. It is how to get type safety over primitive identifiers
without the object overhead, and it is worth using precisely where two Long parameters could be
swapped by mistake.
The boxing does reappear in three places, when the value class is used as a generic type argument, when it is stored in a nullable field, and when it is passed as an interface it implements. That is usually acceptable, the point is the compile-time distinction, and the allocation saving is a bonus on the paths where it holds.
More Kotlin in the Kotlin guides, including inheritance and properties.
Frequently asked questions
What is the difference between val and no keyword in a constructor?
val or var makes the
parameter a property that is stored. Without either it is only a constructor parameter, usable in
initialisers and not retained.
Where does constructor logic go?
In an init block. The primary constructor has no body of its
own.
In what order do init blocks and property initialisers run?
Interleaved, in source order. Not all
properties first, which is why a property can only be read from an init block declared below it.
Why is my overridden property null in the base class’s init?
The subclass’s initialisers have not run yet. Never call or read an open member during construction; pass the value as a constructor parameter.
When do I need a secondary constructor?
When an alternative construction path needs different logic. Different defaults are better expressed with default arguments on the primary.
Does a secondary constructor’s body run before or after init blocks?
After. Delegation to the primary runs every initialiser first.
What is the difference between object and companion object?
object is a standalone singleton.
A companion object is tied to a class and provides what static does in Java.
Is a companion object static in the bytecode?
No. Its members are instance methods on a
Companion class. Java callers need User.Companion.of(...) unless you add @JvmStatic. const val
is the exception and is a genuine static field.
Is an object singleton thread-safe?
Yes. Initialisation happens on first access under the JVM’s class-initialisation lock, so no double-checked locking is needed.
What is the difference between a nested and an inner class?
A nested class holds no reference to
the outer instance; inner opts into one. Kotlin’s default is the opposite of Java’s, and it avoids
a common memory leak.