Kotlin
2026-08-21 09:00
The previous unit was about the calling side.
This one is about declaring the types you call things on.
Same capabilities as Java, less ceremony, plus a few things Java cannot do.
Classes and methods are final unless marked open.
Nested classes hold no outer reference unless marked inner.
Kotlin picks the safer option, and makes you ask for the risk.
internal.when.field.by delegation.static.One colon covers extends and implements.
Where Java’s @Override is optional.
Not pedantry: it prevents accidental override.
You add a method, unknowingly override an inherited one you had forgotten. Java compiles that silently.
The compiler refuses to guess.
Any arbitrary tie-break would silently pick behaviour you did not intend.
Every class is open to inheritance unless marked final.
Kotlin reverses it.
A subclass overrides methods in ways the base author never anticipated,
relying on how the base calls its own methods.
A later, entirely reasonable change to the base breaks the subclass.
A HashSet subclass counting additions.
Overrides add and addAll. Double-counts.
Because addAll happens to call add. Nothing in the contract said so.
Design and document for inheritance, or else prohibit it.
Making final the default means you must decide —
which is exactly the moment you would also document how.
| member | top-level | |
|---|---|---|
public (default) |
everywhere | everywhere |
internal |
the module | the module |
protected |
subclasses | — |
private |
the class | the file |
public is the default. If you did not restrict it, you did not intend to.
No package-private. internal replaces it — and Java’s could be defeated by declaring the same package name from another jar.
protected means subclasses only. Not the same package.
Serialization fails — and names the outer class, not where you were looking.
Memory leaks — the outer object stays alive as long as the nested one does.
Classic in long-lived callbacks and listeners.
Anyone can implement Expr, so the compiler demands an else.
Add a Mul class.
Forget to handle it.
It falls silently into else and throws at runtime, in production.
A when covering all of them needs no else.
Adding a subclass makes every such when fail to compile.
The compiler becomes a checklist of what must be updated.
val or var on a parameter declares a property.
The primary constructor has no body of its own.
A superclass has parentheses. An interface does not.
You construct one and merely implement the other.
Most Java overload families collapse into
one primary constructor with default values.
Reach for secondary constructors mainly at a Java boundary.
interface User { val nickname: String }
class PrivateUser(override val nickname: String) : User // stored
class SubscribingUser(val email: String) : User {
override val nickname get() = email.substringBefore('@') // computed
}
class FacebookUser(val id: Int) : User {
override val nickname = getFacebookName(id) // once
}The interface cannot tell, and does not need to.
The only way to reach the backing field. There is no other name for it.
Only if an accessor references
field, or uses the default.
A computed property stores nothing.
Which is why an extension property must always be computed — nowhere to put one.
Public getter, private setter. Replaces five lines of Java pattern.
Override equals, forget hashCode.
The object vanishes inside a HashSet.
Works perfectly until someone uses it as a map key.
equals · hashCode · toString · copy · componentN
And equals and hashCode are consistent by construction.
Data classes want val properties — immutable instances.
Change then means a new object.
To modify one method of an interface,
implement the whole interface and forward everything else by hand.
Overriding both add and addAll here is not redundant.
CountingSet does not rely on addAll calling add — it delegates.
The fragile base class problem, avoided by composition. One keyword.
Declare a class and create an instance in a single step.
object declaration · companion object · object expression
Everything a class can have, except a constructor.
A singleton with mutable state is a global variable wearing a hat.
Interfering tests, ordering dependencies, no substitution.
Stateless singletons are fine. Stateful ones deserve a hard look.
It can reach the class’s private members — including a private constructor.
That last one: a persistence layer can add Person.Companion.fromJSON
without Person knowing anything about persistence.
May implement several interfaces. May modify enclosing locals.
New instance each time — it is an expression, not a declaration.
Use a lambda.
Which is the next unit.
data and by generate the code you would otherwise write badly.object does three jobs; the companion is the interesting one.Programming with Lambdas begins the functional half.
Lambda syntax, the collection APIs, lazy sequences, Java interop.
And lambdas with receivers — the feature the final DSL unit depends on entirely.
Kotlin · Classes, Objects, and Interfaces