Classes & Properties
The class mechanics behind everyday Kotlin — primary and secondary constructors, initializer order, open versus final inheritance, the internal visibility modifier, nested versus inner classes, backing fields, and default methods on interfaces.
8 questions
JuniorTheoryCommonWhy is a Kotlin class not inheritable unless you mark it open?
Why is a Kotlin class not inheritable unless you mark it open?
Kotlin classes and their members are final by default — the inverse of Java. Subclassing or overriding is opt-in via open. This is deliberate: a class must either be designed for inheritance (documented hooks, a stable contract) or prohibit it, so a subclass cannot accidentally break invariants the author never anticipated.
Common mistakes
- ✗Assuming Kotlin inherits Java's open-by-default rule for classes and members
- ✗Reading
finalby default as a performance trick rather than a design decision - ✗Marking a class
openwhile forgetting its members are stillfinalindividually
Follow-up questions
- →Which declarations are
openimplicitly, without the keyword? - →How do the compiler plugins used by Spring get around
finalclasses?
JuniorTheoryOccasionalWhen does a Kotlin property get a backing field, and what is field?
When does a Kotlin property get a backing field, and what is field?
A property gets a backing field only if its accessors actually use field — the implicit reference to the stored value, visible solely inside get/set. A default accessor uses it, so a plain val/var stores something. A property with a custom getter that computes from other properties (val area get() = w * h) stores nothing at all.
Common mistakes
- ✗Believing every property is backed by a stored field, even a computed one
- ✗Trying to use
fieldoutside a property accessor, where it does not exist - ✗Writing
name = valueinside the setter ofname, which recurses forever
Follow-up questions
- →What happens if a custom setter assigns to the property name instead of
field? - →Why can an interface declare a property but never give it a backing field?
JuniorTheoryOccasionalHow does a primary constructor differ from a secondary one in Kotlin?
How does a primary constructor differ from a secondary one in Kotlin?
The primary constructor sits in the class header — class User(val name: String) — and has no body: its work runs in property initializers and init blocks. A secondary constructor is written in the class body with constructor(...), and must delegate to the primary via this(...) when one exists.
Common mistakes
- ✗Thinking the primary constructor can hold statements instead of
initblocks - ✗Forgetting that a secondary must delegate to the primary with
this(...)when one exists - ✗Reading
val/varin the header as a plain parameter rather than a property declaration
Follow-up questions
- →In what order do property initializers and
initblocks run? - →When is declaring a secondary constructor genuinely necessary in Kotlin?
JuniorTheoryOccasionalWhat does internal make visible, and how is it unlike Java's package-private?
What does internal make visible, and how is it unlike Java's package-private?
internal opens a declaration to the whole compilation module — one Gradle source set, one Maven module — no matter which package it sits in. Java's package-private is the opposite axis: same package only, but across every jar that shares that package name. Kotlin also mangles internal names in bytecode, so Java code cannot call them by accident.
Common mistakes
- ✗Treating
internalas an exact synonym of Java's package-private - ✗Assuming a dependent module can still see an
internaldeclaration - ✗Being surprised that Java sees an
internalmember under a mangled name
Follow-up questions
- →What exactly counts as one module for the purposes of
internal? - →Why does Kotlin mangle the name of an
internalfunction in bytecode?
MiddleDebuggingOccasionalWhy does this class print files=0 when limit is 3?
Why does this class print files=0 when limit is 3?
Property initializers and init blocks run top to bottom, in declaration order, as one primary-constructor body. files = load() runs before limit is assigned, so load() reads limit's default 0 and builds an empty list. The compiler cannot see it because the read goes through a function call. Fix: declare limit above files.
Common mistakes
- ✗Assuming
initblocks run after all property initializers rather than interleaved with them - ✗Expecting the compiler to catch a forward read that goes through a function call
- ✗Thinking a
valis available from the moment the class body opens
Follow-up questions
- →What value does an uninitialized
Intproperty hold while the constructor is still running? - →How would
by lazyonfilesalso fix this without reordering the declarations?
MiddleTheoryOccasionalWhat changes when you add inner to a nested class?
What changes when you add inner to a nested class?
By default a nested class is like a Java static nested class: it holds no reference to the outer instance and cannot touch the outer's properties. inner adds an implicit reference to the enclosing object, so it can read the outer's properties and use this@Outer — at the cost of keeping that outer object alive as long as the inner one lives.
Common mistakes
- ✗Assuming a Kotlin nested class behaves like a Java non-static inner class by default
- ✗Forgetting that
innerpins the outer object in memory, which is a classic leak - ✗Thinking
inneris only about visibility rather than the implicit outer reference
Follow-up questions
- →How does an
innerclass disambiguatethisfrom the outer object'sthis? - →Why is an
innerclass a common cause of a memory leak on Android?
MiddleTheoryOccasionalHow do Kotlin interface method bodies differ from Java 8 default methods?
How do Kotlin interface method bodies differ from Java 8 default methods?
Both ship an implementation on the interface. Kotlin needs no default keyword — you simply write the body — and interface members are implicitly open, unlike class members, so an implementor may override any of them. Kotlin goes further: an interface may declare abstract properties and accessors, while a Java 8 interface holds only constants.
Common mistakes
- ✗Looking for a
defaultkeyword that Kotlin does not have - ✗Assuming interface members are
finalby default like class members - ✗Thinking a Kotlin interface cannot declare a property because Java 8 cannot
Follow-up questions
- →How does a class resolve the same method inherited with a body from two interfaces?
- →Why can an interface property never have a backing field?
SeniorDesignOccasionalA pull request marks one of your core domain classes open and adds open to every one of its members, including two properties with custom getters, so that a unit test can subclass it and stub a couple of methods. The class is used from several modules, and its primary constructor enforces an invariant in an init block. Review the change: explain what marking the class and its members open actually commits you to, why Kotlin makes final the default in the first place, and what you would propose instead so the test can still isolate the collaborator it wants to fake.
A pull request marks one of your core domain classes open and adds open to every one of its members, including two properties with custom getters, so that a unit test can subclass it and stub a couple of methods. The class is used from several modules, and its primary constructor enforces an invariant in an init block. Review the change: explain what marking the class and its members open actually commits you to, why Kotlin makes final the default in the first place, and what you would propose instead so the test can still isolate the collaborator it wants to fake.
open is a permanent public contract: any subclass may replace those members, so the init invariant and both custom getters can be silently overridden, and the class must stay designed for inheritance forever across every module. That is exactly why Kotlin makes members final by default. Instead, extract the collaborator behind an interface and inject it, so the test substitutes a fake without opening production code.
Common mistakes
- ✗Treating
openas a local, reversible convenience rather than a public inheritance contract - ✗Opening production code to make it testable instead of injecting a collaborator behind an interface
- ✗Believing an overridden custom getter cannot break an invariant established in
init
Follow-up questions
- →Which library would let the test fake a
finalclass without opening it, and at what cost? - →How would you keep the class
finaland still let one designated hook be overridden?