Inheritance and interfaces.
Classes let us define types. But types often share structure and behavior. An Employee is a kind of Person. A Manager is a kind of Employee. Today we learn how Kotlin handles these relationships through inheritance and interfaces.
Some concepts are specializations of others.
A car is a vehicle. A bicycle is a vehicle. Both have wheels and can move. But they differ in details.
Without inheritance, you repeat code. The Vehicle code appears in Car and Bicycle separately. Changes require updating multiple places.
With inheritance, you define the common parts once. Vehicle defines wheels and movement. Car and Bicycle inherit these and add their specifics.
Inheritance creates an is-a relationship. A car is a vehicle. A manager is an employee. The subtype can be used wherever the supertype is expected.
In Kotlin, classes are final by default. They cannot be inherited.
This is intentional. Inheritance is powerful but dangerous. A subclass can break assumptions of the parent. Kotlin makes you choose explicitly.
To allow inheritance, mark the class open: open class Vehicle.
Now other classes can extend it: class Car colon Vehicle parentheses.
The colon followed by the parent class establishes inheritance. The parentheses call the parent constructor.
Without open, trying to extend a class produces a compile error. Kotlin protects you from accidental inheritance.
A subclass can provide its own version of a method.
In the parent, mark the method open: open class Vehicle with open fun start.
In the subclass, use override: class Car colon Vehicle with override fun start.
The override keyword is required. It documents your intention and catches errors if the parent method changes.
You can call the parent's version with super: override fun start, super dot start, then additional code.
Overriding lets subclasses customize behavior while reusing the parent structure.
Sometimes a class is too general to instantiate.
You can say "every shape has an area" but cannot define area for a generic Shape. Each specific shape computes it differently.
Abstract classes solve this: abstract class Shape with abstract fun area returning Double.
The abstract keyword means: this cannot be instantiated directly. Subclasses must exist.
Abstract methods have no body. Subclasses must implement them.
class Circle colon Shape with override fun area returning PI times radius squared.
Abstract classes define what subclasses must provide.
Interfaces define contracts without implementation.
interface Clickable with fun click.
Any class can implement an interface: class Button colon Clickable with override fun click.
Interfaces are like abstract classes but more flexible. A class can implement multiple interfaces.
Interfaces can have default implementations: interface Clickable with fun click that prints "Default click".
The class can use the default or override it.
Use interfaces to define capabilities. Clickable means it can be clicked. Comparable means it can be compared. The interface says what, not how.
A class can implement many interfaces.
interface Clickable with fun click. interface Focusable with fun focus.
class Button colon Clickable comma Focusable with override fun click and override fun focus.
This is powerful. A Button is both Clickable and Focusable. It satisfies both contracts.
If two interfaces have the same method with different defaults, you must override and choose: override fun commonMethod with super angle bracket Clickable angle bracket dot commonMethod.
Multiple interface inheritance is safe because interfaces have no state. Diamond problems are resolved explicitly.
Sometimes you want to restrict which classes can inherit.
Sealed classes allow inheritance only within the same file.
sealed class Result. class Success with val data colon Result. class Failure with val error colon Result.
All subclasses must be defined in the same file. No other file can add subclasses.
This enables exhaustive when expressions: when result, is Success arrow use result dot data, is Failure arrow use result dot error. No else needed because all cases are known.
Sealed classes model closed hierarchies. A result is success or failure, nothing else. An expression is literal, variable, or operation, nothing else.
Use sealed classes when you know all the possibilities.
Properties can be abstract or open too.
abstract class Shape with abstract val name String.
class Circle colon Shape with override val name equals "Circle".
You can override a val with a val. You can override a val with a var, adding mutability. You cannot override a var with a val, removing mutability.
Properties can be overridden with custom getters: override val name get returns "My Circle".
The same open and override rules apply to properties as to methods.
Inheritance is powerful but easy to misuse.
Prefer composition over inheritance. Instead of Employee extends Person, consider Employee contains a Person.
Keep hierarchies shallow. Deep inheritance is hard to understand.
Use interfaces for capabilities. Use abstract classes for shared implementation.
Make classes final unless designed for extension. This is Kotlin's default for good reason.
Think is-a carefully. A stack is not a list, even though they seem similar. Wrong inheritance creates fragile designs.
Let us summarize.
Classes are final by default. Mark open to allow inheritance.
Override methods with the override keyword. Call parent with super.
Abstract classes cannot be instantiated. They define what subclasses must implement.
Interfaces define contracts. Classes can implement multiple interfaces.
Sealed classes restrict inheritance to the same file, enabling exhaustive when.
Use inheritance carefully. Prefer composition and shallow hierarchies.