Think of inheritance in programming like a family tree. Just as children inherit traits from their parents, in programming, classes can inherit properties and behaviors from other classes. This allows you to create new classes that build upon existing ones, reusing code and creating a hierarchy of related classes.
In this lesson, we'll explore how to use inheritance and interfaces in Kotlin to create organized, reusable code. We'll learn about subclasses, superclasses, method overriding, and interfaces - the building blocks of object-oriented programming. Understanding these concepts will help you write better, more maintainable code.
Inheritance Basics
When to Use
- When you need to create a new class that is a specialized version of an existing class
- When you want to reuse code from an existing class
- When you need to create a hierarchy of related classes
- When you want to extend functionality without modifying the original class
| Component |
What It Does |
Example |
| Open Class |
Makes a class inheritable |
open class ParentClass { } |
| Subclass Declaration |
Creates a class that inherits from another |
class ChildClass : ParentClass() { } |
| Super Keyword |
Refers to the parent class |
super.methodName() |
| Constructor Call |
Initializes the parent class |
constructor() : super() |
Practical Examples
// ===========================================
// BASIC INHERITANCE EXAMPLE
// ===========================================
// Step 1: Create a base class (parent class)
// The 'open' keyword makes this class inheritable
open class Animal {
// Properties that all animals will have
var name: String = ""
var age: Int = 0
// Constructor - called when creating an Animal object
constructor(name: String, age: Int) {
this.name = name
this.age = age
}
// Method that can be overridden by subclasses
open fun makeSound() {
println("Some animal sound")
}
// Method to display animal info
fun displayInfo() {
println("Name: $name, Age: $age")
}
}
// Step 2: Create a subclass (child class)
// The colon ':' means "inherits from"
// Dog inherits all properties and methods from Animal
class Dog : Animal {
// Additional property specific to dogs
var breed: String = ""
// Constructor for Dog - must call parent constructor
constructor(name: String, age: Int, breed: String) : super(name, age) {
this.breed = breed
}
// Override the makeSound method from Animal
override fun makeSound() {
println("Woof! Woof!")
}
// New method specific to dogs
fun wagTail() {
println("$name is wagging their tail!")
}
}
// ===========================================
// INHERITANCE WITH CONSTRUCTORS EXAMPLE
// ===========================================
// Base class for vehicles
open class Vehicle(val brand: String) {
// Properties
var isRunning: Boolean = false
var currentSpeed: Int = 0
// Method that can be overridden
open fun start() {
isRunning = true
println("Starting $brand vehicle")
}
// Method to stop the vehicle
fun stop() {
isRunning = false
currentSpeed = 0
println("Stopping $brand vehicle")
}
// Method to accelerate
open fun accelerate(speed: Int) {
if (isRunning) {
currentSpeed += speed
println("$brand vehicle accelerating to $currentSpeed mph")
} else {
println("Cannot accelerate - vehicle is not running!")
}
}
}
// Subclass for cars - inherits from Vehicle
class Car(brand: String, val model: String) : Vehicle(brand) {
// Additional properties specific to cars
var numberOfDoors: Int = 4
var fuelType: String = "Gasoline"
// Override the start method to add car-specific behavior
override fun start() {
super.start() // Call the parent class start method first
println("Starting $model car with $numberOfDoors doors")
}
// Override accelerate method for car-specific behavior
override fun accelerate(speed: Int) {
super.accelerate(speed) // Call parent method
if (currentSpeed > 80) {
println("Warning: $model is going fast!")
}
}
// New method specific to cars
fun honkHorn() {
println("$model car honking horn: Beep! Beep!")
}
}
// ===========================================
// USING THE CLASSES EXAMPLE
// ===========================================
// Create and use Animal and Dog objects
println("=== ANIMAL INHERITANCE EXAMPLE ===")
val genericAnimal = Animal("Unknown", 5)
genericAnimal.makeSound() // Outputs: Some animal sound
genericAnimal.displayInfo() // Outputs: Name: Unknown, Age: 5
val myDog = Dog("Buddy", 3, "Golden Retriever")
myDog.makeSound() // Outputs: Woof! Woof! (overridden method)
myDog.displayInfo() // Outputs: Name: Buddy, Age: 3 (inherited method)
myDog.wagTail() // Outputs: Buddy is wagging their tail! (new method)
println("\n=== VEHICLE INHERITANCE EXAMPLE ===")
val genericVehicle = Vehicle("Generic")
genericVehicle.start() // Outputs: Starting Generic vehicle
genericVehicle.accelerate(30) // Outputs: Generic vehicle accelerating to 30 mph
val myCar = Car("Toyota", "Camry")
myCar.start() // Outputs: Starting Toyota vehicle, Starting Camry car with 4 doors
myCar.accelerate(50) // Outputs: Toyota vehicle accelerating to 50 mph
myCar.accelerate(40) // Outputs: Toyota vehicle accelerating to 90 mph, Warning: Camry is going fast!
myCar.honkHorn() // Outputs: Camry car honking horn: Beep! Beep!
Method Overriding
When to Use
- When you need to customize behavior inherited from a parent class
- When you want to extend functionality of a parent method
- When you need to provide a different implementation for a method
| Component |
What It Does |
When to Use It |
| Open Method |
Makes a method overridable |
When you want to allow subclasses to customize behavior |
| Override Keyword |
Indicates a method is overriding a parent method |
When implementing a custom version of a parent method |
| Super Call |
Calls the parent class method |
When you want to extend rather than replace parent behavior |
Practical Examples
// ===========================================
// METHOD OVERRIDING WITH BANK ACCOUNTS
// ===========================================
// Base class for all bank accounts
open class BankAccount(val accountNumber: Int, var balance: Double) {
// Properties
val accountType: String = "Basic Account"
var isActive: Boolean = true
// Method that can be overridden
open fun displayBalance() {
println("Account $accountNumber ($accountType): Balance is $${String.format("%.2f", balance)}")
}
// Method to deposit money
open fun deposit(amount: Double) {
if (amount > 0 && isActive) {
balance += amount
println("Deposited $${String.format("%.2f", amount)} to account $accountNumber")
println("New balance: $${String.format("%.2f", balance)}")
} else {
println("Invalid deposit amount or account inactive")
}
}
// Method to withdraw money
open fun withdraw(amount: Double): Boolean {
if (amount > 0 && amount <= balance && isActive) {
balance -= amount
println("Withdrew $${String.format("%.2f", amount)} from account $accountNumber")
println("New balance: $${String.format("%.2f", balance)}")
return true
} else {
println("Insufficient funds, invalid amount, or account inactive")
return false
}
}
}
// Subclass for savings accounts
class SavingsAccount(accountNumber: Int, balance: Double, val interestRate: Double)
: BankAccount(accountNumber, balance) {
// Override the accountType property
override val accountType: String = "Savings Account"
// Override displayBalance to show additional info
override fun displayBalance() {
super.displayBalance() // Call parent method first to show basic info
println(" Interest Rate: ${(interestRate * 100)}%") // Add savings-specific info
println(" Interest earned this period: $${String.format("%.2f", calculateInterest())}")
}
// Override deposit to add interest calculation
override fun deposit(amount: Double) {
super.deposit(amount) // Use parent method for basic deposit
// Add interest calculation for savings
val interest = calculateInterest()
if (interest > 0) {
println(" Interest added: $${String.format("%.2f", interest)}")
}
}
// Override withdraw to add savings-specific logic
override fun withdraw(amount: Double): Boolean {
// Check if withdrawal would leave minimum balance
if (balance - amount < 100) {
println("Warning: This withdrawal would leave less than $100 minimum balance")
}
// Use parent method for actual withdrawal
return super.withdraw(amount)
}
// New method specific to savings accounts
fun calculateInterest(): Double {
return balance * interestRate
}
// Method to add interest to balance
fun addInterest() {
val interest = calculateInterest()
balance += interest
println("Interest of $${String.format("%.2f", interest)} added to savings account")
}
}
// Subclass for checking accounts
class CheckingAccount(accountNumber: Int, balance: Double, val monthlyFee: Double)
: BankAccount(accountNumber, balance) {
override val accountType: String = "Checking Account"
// Override displayBalance to show fees
override fun displayBalance() {
super.displayBalance() // Call parent method
println(" Monthly fee: $${String.format("%.2f", monthlyFee)}")
}
// Override withdraw to add checking-specific logic
override fun withdraw(amount: Double): Boolean {
// Check if withdrawal would cause overdraft
if (amount > balance) {
println("Warning: This withdrawal will cause overdraft!")
}
return super.withdraw(amount)
}
// New method to apply monthly fee
fun applyMonthlyFee() {
balance -= monthlyFee
println("Monthly fee of $${String.format("%.2f", monthlyFee)} applied to checking account")
}
}
// ===========================================
// USING THE OVERRIDDEN METHODS
// ===========================================
println("=== BANK ACCOUNT OVERRIDING EXAMPLE ===")
// Create different types of accounts
val basicAccount = BankAccount(1001, 500.0)
val savingsAccount = SavingsAccount(1002, 1000.0, 0.025) // 2.5% interest
val checkingAccount = CheckingAccount(1003, 750.0, 5.0) // $5 monthly fee
// Test basic account
println("--- Basic Account ---")
basicAccount.displayBalance()
basicAccount.deposit(200.0)
basicAccount.withdraw(100.0)
// Test savings account with overridden methods
println("\n--- Savings Account ---")
savingsAccount.displayBalance() // Shows interest rate and earned interest
savingsAccount.deposit(500.0) // Shows interest calculation
savingsAccount.addInterest() // Adds interest to balance
savingsAccount.displayBalance() // Shows updated balance with interest
// Test checking account with overridden methods
println("\n--- Checking Account ---")
checkingAccount.displayBalance() // Shows monthly fee
checkingAccount.withdraw(800.0) // Shows overdraft warning
checkingAccount.applyMonthlyFee() // Applies monthly fee
checkingAccount.displayBalance() // Shows balance after fee
Interfaces
When to Use
- When you need to define a contract that multiple classes can implement
- When you want to ensure certain methods are available in a class
- When you need to support multiple inheritance of behavior
| Feature |
What It Does |
When to Use It |
| Interface Declaration |
Defines a contract of methods |
When you need to define common behavior |
| Interface Implementation |
Makes a class implement an interface |
When you want to ensure certain methods are available |
| Multiple Interfaces |
Allows a class to implement multiple interfaces |
When you need to combine different behaviors |
Practical Examples
// ===========================================
// INTERFACE BASICS EXAMPLE
// ===========================================
// Interface defines what methods a class MUST have
// Think of it as a "contract" or "promise"
interface Drivable {
// Any class that implements Drivable MUST have these methods
fun drive()
fun stop()
fun getSpeed(): Int
}
interface Flyable {
fun fly()
fun land()
fun getAltitude(): Int
}
interface Swimmable {
fun swim()
fun dive()
fun getDepth(): Int
}
// ===========================================
// IMPLEMENTING SINGLE INTERFACE
// ===========================================
// Car implements only the Drivable interface
class Car : Drivable {
private var currentSpeed: Int = 0
private var isEngineRunning: Boolean = false
// MUST implement all methods from Drivable interface
override fun drive() {
if (isEngineRunning) {
currentSpeed = 60
println("Car is driving at $currentSpeed mph")
} else {
println("Cannot drive - engine is not running!")
}
}
override fun stop() {
currentSpeed = 0
println("Car has stopped")
}
override fun getSpeed(): Int {
return currentSpeed
}
// Additional methods specific to cars
fun startEngine() {
isEngineRunning = true
println("Car engine started")
}
fun turnOffEngine() {
isEngineRunning = false
currentSpeed = 0
println("Car engine turned off")
}
}
// ===========================================
// IMPLEMENTING MULTIPLE INTERFACES
// ===========================================
// Duck can implement multiple interfaces
// This gives it the behavior of multiple different types
class Duck : Drivable, Flyable, Swimmable {
private var currentSpeed: Int = 0
private var currentAltitude: Int = 0
private var currentDepth: Int = 0
private var isFlying: Boolean = false
private var isSwimming: Boolean = false
// Implement Drivable interface methods
override fun drive() {
// Ducks don't really "drive" but we can make them walk
currentSpeed = 5
println("Duck is waddling at $currentSpeed mph")
}
override fun stop() {
currentSpeed = 0
println("Duck has stopped waddling")
}
override fun getSpeed(): Int {
return currentSpeed
}
// Implement Flyable interface methods
override fun fly() {
isFlying = true
currentAltitude = 100
println("Duck is flying at altitude $currentAltitude feet")
}
override fun land() {
isFlying = false
currentAltitude = 0
println("Duck has landed")
}
override fun getAltitude(): Int {
return currentAltitude
}
// Implement Swimmable interface methods
override fun swim() {
isSwimming = true
currentDepth = 0
println("Duck is swimming on the surface")
}
override fun dive() {
if (isSwimming) {
currentDepth = 10
println("Duck is diving to depth $currentDepth feet")
} else {
println("Duck must be swimming before diving!")
}
}
override fun getDepth(): Int {
return currentDepth
}
// Additional methods specific to ducks
fun quack() {
println("Duck says: Quack! Quack!")
}
fun flapWings() {
if (isFlying) {
println("Duck is flapping wings while flying")
} else {
println("Duck is flapping wings on the ground")
}
}
}
// ===========================================
// USING INTERFACES FOR POLYMORPHISM
// ===========================================
// Function that works with ANY Drivable object
fun testDriving(vehicle: Drivable) {
println("Testing driving capabilities...")
vehicle.drive()
println("Current speed: ${vehicle.getSpeed()} mph")
vehicle.stop()
println("---")
}
// Function that works with ANY Flyable object
fun testFlying(aircraft: Flyable) {
println("Testing flying capabilities...")
aircraft.fly()
println("Current altitude: ${aircraft.getAltitude()} feet")
aircraft.land()
println("---")
}
// Function that works with ANY Swimmable object
fun testSwimming(swimmer: Swimmable) {
println("Testing swimming capabilities...")
swimmer.swim()
swimmer.dive()
println("Current depth: ${swimmer.getDepth()} feet")
println("---")
}
// ===========================================
// USING THE INTERFACE-BASED CLASSES
// ===========================================
println("=== INTERFACE IMPLEMENTATION EXAMPLE ===")
// Create objects
val car = Car()
val duck = Duck()
// Test car (implements only Drivable)
println("--- Testing Car ---")
car.startEngine()
testDriving(car) // Car can be used anywhere a Drivable is expected
car.turnOffEngine()
// Test duck (implements multiple interfaces)
println("\n--- Testing Duck ---")
duck.quack()
testDriving(duck) // Duck can be used as a Drivable
testFlying(duck) // Duck can be used as a Flyable
testSwimming(duck) // Duck can be used as a Swimmable
// Show how interfaces enable polymorphism
println("\n=== POLYMORPHISM EXAMPLE ===")
val drivableObjects: List = listOf(car, duck)
val flyableObjects: List = listOf(duck)
val swimmableObjects: List = listOf(duck)
println("All drivable objects:")
for (obj in drivableObjects) {
testDriving(obj)
}
println("All flyable objects:")
for (obj in flyableObjects) {
testFlying(obj)
}