Table of Contents
Reusing and Extending Behavior
Inheritance in Kotlin is a way to create a new class that reuses, extends, or modifies the behavior of an existing class. The existing class is called the superclass or base class. The new class is called the subclass or derived class.
In Kotlin, classes are final by default. You must explicitly mark a class as open if you want to inherit from it. This is one of the most important differences from some other languages where everything is inheritable unless you restrict it.
To inherit from a class in Kotlin, the base class must be marked open and the subclass must use a colon : followed by the base class name.
For example, if you have a base class Animal and you want to create a more specific type Dog, you would write:
open class Animal {
fun eat() {
println("Animal is eating")
}
}
class Dog : Animal()
Here Dog inherits all accessible properties and functions from Animal. In this simple example, Dog can directly call eat() without having to reimplement it.
Open and Final Members
Just like classes, member functions and properties are also final by default. If you want a subclass to be able to change the behavior of a function or property, you must mark that member as open in the base class.
In the subclass, you then use override to replace that behavior with a more specific version.
open class Animal {
open fun makeSound() {
println("Some generic animal sound")
}
fun sleep() {
println("Animal is sleeping")
}
}
class Dog : Animal() {
override fun makeSound() {
println("Woof!")
}
}
In this example, makeSound is open, so Dog can override it. The sleep function is not open, so it remains unchanged and cannot be overridden by subclasses. The override keyword is required and makes it very clear which functions come from the base class.
Calling the Superclass Implementation
Sometimes you want to extend behavior instead of completely replacing it. Kotlin lets you call the base implementation from within the overridden function by using super.
open class Animal {
open fun makeSound() {
println("Some generic animal sound")
}
}
class Dog : Animal() {
override fun makeSound() {
super.makeSound()
println("Dog adds: Woof!")
}
}
Here the Dog implementation calls super.makeSound() first. This allows you to keep the base behavior and then add more specific behavior afterward. You can choose to call super at the start, in the middle, at the end, or not at all, depending on what you need.
Inheritance and Constructors
When a subclass inherits from a class that has a constructor, it must call one of the base class constructors. This is done in the class header after the colon. Which constructors are available depends on how the base class is defined.
For a base class with a primary constructor:
open class Animal(val name: String) {
open fun describe() {
println("Animal name: $name")
}
}
class Dog(name: String, val breed: String) : Animal(name) {
override fun describe() {
println("Dog name: $name, breed: $breed")
}
}
Dog has its own primary constructor and passes name to the Animal constructor using : Animal(name). This ensures the base class is correctly initialized before the subclass adds its own details.
If the base class defines secondary constructors, subclasses can choose which one to call, but there must always be a clear chain of constructor calls that ultimately initialize the base part of the object.
Class Hierarchies and Abstraction
When you design class hierarchies, you often think from general to specific. A base class represents the general concept, and subclasses represent concrete specializations. For example, Animal might be general, while Dog and Cat are more specific.
You usually place common properties and behavior in the base class. Each subclass then adds or specializes behavior that is unique to that type. This arrangement becomes especially powerful when combined with polymorphism, because you can work with the general type and still get the specific behavior.
What Polymorphism Means
Polymorphism literally means "many forms". In object oriented programming, it lets you treat objects of different subclasses as if they are instances of the same base type, while still using their most specific behavior.
In Kotlin, this most often appears when a variable or function parameter has the type of the base class, but it actually refers to an instance of a subclass. When you call an overridden function on that variable, Kotlin automatically selects the correct subclass implementation at runtime.
With polymorphism, the method that is executed depends on the actual object type at runtime, not just on the variable type.
For example:
open class Animal {
open fun makeSound() {
println("Some generic animal sound")
}
}
class Dog : Animal() {
override fun makeSound() {
println("Woof!")
}
}
class Cat : Animal() {
override fun makeSound() {
println("Meow!")
}
}
fun playWithAnimal(animal: Animal) {
animal.makeSound()
}If you call:
playWithAnimal(Dog())
playWithAnimal(Cat())
Both calls use the same parameter type Animal, but the behavior is different. The first call prints "Woof!", and the second prints "Meow!". This is runtime polymorphism through method overriding.
Overriding vs Overloading
Overriding and overloading sound similar but are different concepts. Overriding is what happens in inheritance when a subclass provides its own implementation for an open function from the base class, using the override keyword.
Overloading means you have multiple functions with the same name in the same scope but with different parameter lists. Overloading does not depend on inheritance and does not require the override keyword. Kotlin decides which overloaded function to call based on the parameter types and count at compile time.
Polymorphism, in the sense used here, is mainly about overriding and dynamic method dispatch, not overloading.
The Role of `is` and Smart Casts
Often when you work with polymorphism, you have a reference of a base type, but you want to perform subclass-specific logic in some cases. Kotlin provides the is operator to check whether an object is of a certain type. When the check succeeds inside the correct branch, Kotlin performs a smart cast, which means you can use the object as if it already has the more specific type.
For example:
fun describeAnimal(animal: Animal) {
if (animal is Dog) {
// smart cast to Dog, no explicit cast needed
println("This is a dog")
} else if (animal is Cat) {
println("This is a cat")
} else {
println("Unknown animal")
}
}This technique is useful if you cannot express certain behavior through the base class interface, although in a well designed hierarchy, you often rely primarily on overridden functions rather than type checks.
Upcasting and Downcasting
Assigning a subclass object to a variable of the base type is called upcasting. This is always safe. It happens automatically without any special syntax and is the basis for polymorphism.
For example:
val dog: Dog = Dog()
val animal: Animal = dog
Here animal is a reference of type Animal that points to a Dog object.
Going in the opposite direction, from a base type to a subclass type, is called downcasting. This is only safe if the actual object really is an instance of that subclass. Kotlin provides the as operator for explicit casts, and the safe cast operator as? which returns null if the cast is not possible.
val animal: Animal = Dog()
val dog1: Dog = animal as Dog // throws if not a Dog
val dog2: Dog? = animal as? Dog // null if not a DogFor Android development, you often see safe casts when dealing with views or system services where the exact type may vary.
Virtual Methods and Dynamic Dispatch
In Kotlin, all open functions are virtual. This means that when you call them on a variable of a base type, the runtime decides which implementation to execute based on the actual object type.
This process is called dynamic dispatch. It is what enables polymorphism. If a function is not open, there is no overriding and the call is statically bound to that specific implementation at compile time.
In practice, when you define base classes intended for extension, you choose carefully which functions to mark as open. Every open function can be changed by subclasses, which is powerful but also means you must think about how it will be used.
Polymorphism in Practice for Android
While this chapter does not explain Android classes in detail, it is useful to understand that many Android APIs rely on inheritance and polymorphism. For example, core types in the Android framework are designed as class hierarchies. You frequently pass around references of a base type and work with subclasses without always knowing their exact concrete type.
In Android development, this pattern allows you to write general code that can work with different visual components or systems, and let the specific behavior depend on the concrete classes that the framework or your code provides at runtime.