Classes & Properties
A Kotlin class is a header and a body, and almost the whole topic lives on the seam between them. The header carries the primary constructor — which has no body at all, and where a val/var in the parameter list is not a parameter but a property declaration. The compiler does not run the body haphazardly: it stitches every property initializer and every init block into one primary-constructor body and runs them strictly in declaration order, interleaved. Hence the flagship trap of the topic: a property read before its own initializer is not a compile error — it silently hands back its default, 0 or null.
The second dividing line is property versus field. A val/var compiles to accessors, and a backing field is added only when it is needed: it exists only if the default accessor is kept or the magic field identifier is referenced inside a custom get/set. So a computed val with a custom getter stores nothing and is recomputed on every access, and an interface may declare a property yet never give it a field — an interface holds no state. Then come the decisions where Kotlin deliberately inverted Java: classes and members are final by default here (open is not a convenience but a permanent public inheritance contract), a nested class holds no reference to the outer object by default (inner is the opt-in to that reference and to the memory leak that rides along with it), and internal opens a declaration to the compilation module, not the package, because Kotlin has no package-private at all. The layer-by-layer breakdown is below.
Topic map
- Primary and secondary constructors — the bodyless header, a
valin the header as a property declaration, mandatory delegation viathis(...), and why default arguments usually beat a secondary constructor. - Initializer order — initializers and
initas one constructor body, the default value before assignment, and calling anopenmember from a base constructor. - Properties and the backing field — when a field is created at all, what
fieldis, infinite recursion in a custom setter,private set, andconst val. - open versus final —
finalby default as a fix for the fragile base class,openon a class not opening its members,final override, and the cost of opening code for tests. - Visibility and internal — the compilation module instead of the package, no package-private, name mangling in the bytecode, and
private constructor. - Nested and inner classes — a nested class with no outer reference,
innerandthis@Outer, the classic Android leak, and the inversion of Java's rule. - Interfaces with implementations — a method body with no
defaultkeyword, implicitlyopenmembers, stateless properties, and resolving the diamond withsuper<A>.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
| Thinking the primary constructor can hold statements | It has no body: its work runs in property initializers and init blocks |
Reading a header val/var as an ordinary parameter | It is a property declaration; a bare name: String is visible only in init and initializers, then gone |
Forgetting a secondary must delegate via this(...) | Compile error: when a primary exists, it cannot be bypassed |
Assuming init blocks run after all initializers | They run interleaved, strictly in declaration order, as one constructor body |
| Expecting the compiler to catch a forward read through a function call | It catches a direct val a = b, but not a read through load() — you silently get 0 or null |
Calling an open member from a base-class constructor | The base body runs first — the overridden member sees the subclass's fields uninitialized |
| Thinking every property has a stored field behind it | A backing field is created only when the default accessor is used or field is referenced |
Trying to use field outside an accessor | The identifier exists only inside its property's get/set |
Writing name = value inside the setter of name | The setter calls itself — infinite recursion and a StackOverflowError |
| Assuming Kotlin inherited Java's open-by-default rule | Classes and members are final by default; inheritance is opt-in via open |
Marking a class open and expecting its members to open too | Each member stays final until its own open; only the class itself is open |
Reading final by default as a performance trick | It is a design decision: protection from the fragile base class, not a JIT hint |
| Opening a production class to make it testable | open is a permanent public contract across every module; the fix is to extract the collaborator behind an interface |
Treating internal as a synonym for package-private | internal is the whole compilation module regardless of package; Kotlin has no package-private at all |
Assuming a dependent module can still see an internal declaration | It cannot: the boundary is the module, not the dependency graph |
Being surprised Java sees an internal member under a mangled name | In bytecode it is public with a $module_name suffix — hence the "ugly" name |
| Assuming a nested class behaves like a Java inner class by default | By default it holds no reference to the outer object — the inverse of Java's rule |
Forgetting that inner pins the outer object in memory | The classic leak: an inner listener outlives the Activity and holds its whole graph |
Hunting for a default keyword on a Kotlin interface method | There is none: an interface method simply carries a body |
Assuming interface members are final by default like class members | They are implicitly open — an implementor may override any of them |
| Expecting an interface property to store something | It has no backing field: an interface holds no state, it only demands it |
Interview relevance
The topic looks "junior" right until the interviewer shows a fifteen-line class and asks why it prints 0. What they probe is not vocabulary but the execution model: exactly what the compiler does with a class body, at which moment a field receives its value, what physically sits behind the word "property", and why open is a promise rather than a modifier. A candidate who answers "init runs when the object is created" is right and has answered nothing.
Typical checks:
- How the primary constructor differs from a secondary one, and when
this(...)is mandatory. - The order in which property initializers and
initblocks run — and what a prematurely read property returns. - What happens when a base-class constructor calls an
openmethod overridden in a subclass. - When a property gets a backing field, what
fieldis, and how a custom setter goes wrong. - Why Kotlin makes
finalthe default, and whatopenon a class and its members commits you to. - What exactly
internalopens, and how it is unlike Java's package-private. - What
innerchanges about a nested class, and why it is a classic source of a memory leak. - How Kotlin interface implementations differ from Java 8
defaultmethods, and how the diamond is resolved.
Common wrong answer: "first all the properties are initialized, then the init blocks run." That mental model implies declaration order does not matter — so the candidate confidently puts val files = load() above val limit = 3 and then hunts for the bug inside load(). In reality initializers and init are one constructor body run top to bottom in declaration order: load() reads a limit that is not yet assigned and gets 0. The second classic failure is "open costs nothing, it just permits subclassing." It does cost: any overridden member, a custom getter included, can silently break an invariant checked in init, and you cannot take open back — it is a public contract across every module that has already relied on it.