Kotlin Training Program

DOWNLOAD APP

FEEDBACK

Inheritance

Inheritance is a mechanism through which a class can extend or reuse members of another class.

Inheritance is an OOPs feature that lets us extend a class’s members & functionality and provides a way to reuse existing code. Let us understand the need of Inheritance with the help of an example.

Ex: Bank Account

Problem Statement

Lets implement an oversimplified example of a Banking system. This bank supports three types of Bank Accounts - Savings, Current & Loan.

Attributes

  • Each type of bank account has the following attributes :
    • accountHolder: String (name of account holder)
    • balance: Float (current balance in account)
    • isActive: Boolean (whether the account is active)
    • openedOn: String (date in dd/mm/yyyy format)
    • transactions: List<Transaction> (transactions of the account)
  • Additionally,
    • Savings account has one more attribute :
      • interestRate: Float (interest rate in percentage)
    • Current account has two more attributes :
      • firmName: String
      • firmRegistrationNo: String
    • Loan account has four more attributes :
      • loanAmount: Float
      • loanPeriodInMonths: Int
      • loanInterestRate: Float
      • loanAmountPaid: Float
  • A Transaction is defined by following attributes :
    • date: String
    • amount: Float
    • description: String

Functionalities

Simulation of the following operations have to be implemented for all three type of accounts :

  • Cash deposit (prevent if account is inactive)
  • Cash withdrawal (prevent if insufficient balance or account inactive)
  • Printing account statement with each transaction in the format : $date → Rs. $amount : DEBIT / CREDIT → $description
  • close account

Additionally,

  • For savings account, simulate interest deposit
  • For current account, simulate checking minimum balance and debit fine if found below that
  • For loan account, simulate loan recovery / EMI payment and debit fine if insufficient funds

Examples

Savings Account

Input :

 accountHolder = A
interestRate = 4%

Operations :
Withdraw Rs. 1000
Deposit Rs. 5000
Deposit Interest
Print Statement
 

Output :

 Insufficient balance (Rs. 0.0) to withdraw (Rs. 1000.0)!

---- : ACCOUNT STATEMENT for A : ----
01/02/2023 -> Rs. 0.0 : CREDIT -> Opening Balance
01/02/2023 -> Rs. 5000.0 : CREDIT -> Cash Deposit
01/02/2023 -> Rs. 200.0 : CREDIT -> Interest for Feb 2023
01/02/2023 -> Rs. 5200.0 -> Closing Balance
 

Current Account

Input :

 accountHolder = B
firmName = ABC Enterprises
firmRegistrationNo = RJ14-123456

Operations :
Check Minimum Balance
Deposit Rs. 6000
Close Account
Withdraw Rs. 1000
Print Statement
 

Output :

 Can't withdraw cash from an inactive account!

---- : ACCOUNT STATEMENT for B : ----
01/02/2023 -> Rs. 0.0 : CREDIT -> Opening Balance
01/02/2023 -> Rs. -100.0 : DEBIT -> Minimum balance Fine for Feb 2023
01/02/2023 -> Rs. 6000.0 : CREDIT -> Cash Deposit
01/02/2023 -> Rs. 5900.0 -> Closing Balance
 

Loan Account

Input :

 accountHolder = C
loanAmount = 5,00,000
loanPeriodInMonths = 5 * 12
loanInterestRate = 8%

Operations :
Deposit Rs. 10,000
2 * Perform Loan Recovery
Deposit Rs. 20,000
4 * Perform Loan Recovery
Print Statement
 

Output :

 Loan of Rs. 500000.0 @ 8.0% : EMI of Rs. 9000.0 : 3 / 60 paid

---- : ACCOUNT STATEMENT for C : ----
01/02/2023 -> Rs. 0.0 : CREDIT -> Opening Balance
01/02/2023 -> Rs. 10000.0 : CREDIT -> Cash Deposit
01/02/2023 -> Rs. -9000.0 : DEBIT -> Loan recovery (Feb 2023)
01/02/2023 -> Rs. -100.0 : DEBIT -> Insufficient balance for EMI (Feb 2023) Fine
01/02/2023 -> Rs. 20000.0 : CREDIT -> Cash Deposit
01/02/2023 -> Rs. -9000.0 : DEBIT -> Loan recovery (Feb 2023)
01/02/2023 -> Rs. -9000.0 : DEBIT -> Loan recovery (Feb 2023)
01/02/2023 -> Rs. -100.0 : DEBIT -> Insufficient balance for EMI (Feb 2023) Fine
01/02/2023 -> Rs. -100.0 : DEBIT -> Insufficient balance for EMI (Feb 2023) Fine
01/02/2023 -> Rs. 2700.0 -> Closing Balance
 

Naive approach

A naive approach to this problem is to create a common class - BankAccount for all three types of accounts. But it leads to several issues which we will see in a while.

 class BankAccount(

    val accountType: String,

    /* Common Fields */
    val accountHolder: String,
    var balance: Float,
    var isActive: Boolean,
    val openedOn: String,
    val transactions: MutableList<Transaction>,

    /* Savings Account Fields */
    val interestRate: Float? = null,

    /* Current Account Fields */
    val firmName: String? = null,
    val firmRegistrationNo: String? = null,

    /* Loan Account Fields */
    val loanAmount: Float? = null,
    val loanPeriodInMonths: Int? = null,
    val loanInterestRate: Float? = null,
    var loanAmountPaid: Float? = null
) {

    /* Common Functions */
    fun deposit(amount: Float) { TODO() }
		fun withdraw(amount: Float) { TODO() }
		fun printStatement() { TODO() }
		fun close() { TODO() }

		/* Savings Account Functions */
    fun depositInterest() { TODO() }
		
		/* Current Account Functions */
    fun checkMinimumBalance() { TODO() }

		/* Loan Account Functions */
    fun performLoanRecovery() { TODO() }
}
 

Note that we have created a new property for account type - accountType: String which can take either of the following values SAVINGS, CURRENT and LOAN.

We can set default values for the following properties :

 class BankAccount(
		/* ... */

    var balance: Float = 0f,
    var isActive: Boolean = true,
    val openedOn: String = getCurrentDate(),
    val transactions: MutableList<Transaction> = mutableListOf()

		/* ... */
)
 

So, by default every account (if not specified)

  • gets 0 balance,
  • is active,
  • gets openedOn date by getCurrentDate() function,
  • transactions is initialized to an empty list

Here is how to create each type of account :

 // Creates a savings account
val savingsAccount = BankAccount(
    accountType = "SAVINGS",
    accountHolder = "A",
    interestRate = 4f
)

// Creates a current account
val currentAccount = BankAccount(
    accountType = "CURRENT",
    accountHolder = "B",
    firmName = "ABC Enterprises",
    firmRegistrationNo = "RJ14-123456"
)

// Creates a loan account
val loanAccount = BankAccount(
    accountType = "LOAN",
    accountHolder = "C",
    loanAmount = 5_00_000f,
    loanAmountPaid = 0f,
    loanPeriodInMonths = 5 * 12,
    loanInterestRate = 8f
)
 

Problems in this approach

Invalid values for accountType

Invalid values for accountType can be entered. accountType is a String, so it can accept any value apart from SAVINGS, CURRENT and LOAN also. For example, following code runs without any errors :

 val account = BankAccount(
    accountType = "STAR",
    accountHolder = "A",
    interestRate = 4f
)
 

We can solve this problem by adding a check for this property in init block and making this field immutable using val :

 init {
    val validAccountTypes = listOf("SAVINGS", "CURRENT", "LOAN")
    if (accountType !in validAccountTypes) {
        error("Invalid account type: ($accountType), must be one of $validAccountTypes")
    }
}
 

No type based restrictions on properties

While creating a SAVINGS account object, one can easily define properties specific to CURRENT account :

 val account = BankAccount(
    accountType = "SAVINGS",
    accountHolder = "A",
    interestRate = 4f,

    // CURRENT account properties
    firmName = "ABC Enterprises",
    firmRegistrationNo = "RJ14-123456"
)
 

firmName & firmRegistrationNo are specific to CURRENT account only yet here we are able to define them for SAVINGS account also. This leads to inconsistent data. There is no rigid boundary between accountTypes.

A workaround for this problem is to use private Access Modifier & Constructor Overloading :

 class BankAccount(

    val accountType: String,

    /* Common Fields */
    val accountHolder: String,
    var balance: Float = 0f,
    var isActive: Boolean = true,
    val openedOn: String = getCurrentDate(),
    val transactions: MutableList<Transaction> = mutableListOf(
        Transaction(getCurrentDate(), balance, "Opening Balance")
    )
) {
    /* Savings Account Fields */
    private var interestRate: Float? = null

    /* Current Account Fields */
    private var firmName: String? = null
    private var firmRegistrationNo: String? = null

    /* Loan Account Fields */
    private var loanAmount: Float? = null
    private var loanPeriodInMonths: Int? = null
    private var loanInterestRate: Float? = null
    private var loanAmountPaid: Float? = null

    /* Constructors */

    // For SAVINGS account
    constructor(accountHolder: String, interestRate: Float): this(
        accountType = "SAVINGS",
        accountHolder = accountHolder
    ) {
        this.interestRate = interestRate
    }

    // For CURRENT account
    constructor(accountHolder: String, firmName: String, firmRegistrationNo: String): this(
        accountType = "CURRENT",
        accountHolder = accountHolder
    ) {
        this.firmName = firmName
        this.firmRegistrationNo = firmRegistrationNo
    }

    // For LOAN account
    constructor(
        accountHolder: String,
        loanAmount: Float,
        loanPeriodInMonths: Int,
        loanInterestRate: Float
    ): this(
        accountType = "LOAN",
        accountHolder = accountHolder
    ) {
        this.loanAmount = loanAmount
        this.loanPeriodInMonths = loanPeriodInMonths
        this.loanInterestRate = loanInterestRate
        loanAmountPaid = 0f
    }

		// ... Other functions
}
 

Note :

  • All common properties are arguments to the primary constrcutor.

  • For each accountType, we have a secondary constrcutor defined using constructor overloading. Also, accountType is automatically set in each secondary constructor.

  • All the account-type specific properties are defined as private and inside the class body. This way, changing properties specific to accountType is now restricted :

     val savingsAccount = BankAccount(
        accountHolder = "A",
        interestRate = 4f
    )
    
    // ERROR! Not allowed because firmName is private
    savingsAccount.firmName = "ABC Enterprises"
     

No type based restrictions on functions

Functions specific to SAVINGS account can easily be invoked using a CURRENT account object. For example, following code runs without any errors :

 val currentAccount = BankAccount(
    accountType = "CURRENT",
    accountHolder = "B",
    firmName = "ABC Enterprises",
    firmRegistrationNo = "RJ14-123456"
)

currentAccount.depositInterest()
 

This shouldn’t happen, it is incorrect - interest should only be deposited in SAVINGS account and not CURRENT account!

Similarly, there is no clear boundary between all account types and hence gives rise to many logically incorrect operations.

One workaround for this problem would be to add GuardCode in all accountType-specific functions to check the type first. Example :

 fun depositInterest() {
		// GuardCode for type checking
    if (accountType != "SAVINGS") {
        error("This operation can only be performed on SAVINGS account.")
    }

    if (isActive) {
        transact(balance * interestRate!! / 100, "Interest for ${getCurrentMonth()}")
    }
}
 

Complex code

The common class approach has many problems and we have seen workarounds for each of them (GuardCode, Secondary constructors). These workarounds make the code complex. Moreover, we have only one class for all three account types making it hard to navigate across the code.

When more features are added, this complexity increases and hence decreases code readability & maintainability.

Conclusion

This problem would have been easy if account specific fields and functionalities weren’t there. For example, field interestRate and functionality depositInterest() are specific to Savings account only. Creating a common class exposes this field and functionality to non-Savings account also. Moreover interestRate is not even defined for other accounts, then having it in the object of (say) Current account doesn’t make any sense.

Separate class approach

The major issue with common class approach was of no clear boundaries between each of the accountTypes. So, lets define separate classes for each of the accountType.

 class SavingsAccount(
		/* Common Fields */
    val accountHolder: String,
    var balance: Float,
    var isActive: Boolean,
    val openedOn: String,
    val transactions: MutableList<Transaction>,

		/* Savings Account Fields */
    val interestRate: Float
) {
		/* Common Functions */
    fun deposit(amount: Float) { TODO() }
		fun withdraw(amount: Float) { TODO() }
		fun printStatement() { TODO() }
		fun close() { TODO() }

		/* Savings Account Functions */
    fun depositInterest() { TODO() }
}
 
 class CurrentAccount(
    /* Common Fields */
    val accountHolder: String,
    var balance: Float,
    var isActive: Boolean,
    val openedOn: String,
    val transactions: MutableList<Transaction>,

		/* Current Account Fields */
    val firmName: String,
    val firmRegistrationNo: String
) {
		/* Common Functions */
    fun deposit(amount: Float) { TODO() }
		fun withdraw(amount: Float) { TODO() }
		fun printStatement() { TODO() }
		fun close() { TODO() }
		
		/* Current Account Functions */
    fun checkMinimumBalance() { TODO() }
}
 
 class LoanAccount(
    /* Common Fields */
    val accountHolder: String,
    var balance: Float,
    var isActive: Boolean,
    val openedOn: String,
    val transactions: MutableList<Transaction>,

		/* Loan Account Fields */
    val loanAmount: Float,
    val loanPeriodInMonths: Int,
    val loanInterestRate: Float,
    var loanAmountPaid: Float = 0f
) {
		/* Common Functions */
    fun deposit(amount: Float) { TODO() }
		fun withdraw(amount: Float) { TODO() }
		fun printStatement() { TODO() }
		fun close() { TODO() }

		/* Loan Account Functions */
    fun performLoanRecovery() { TODO() }
}
 

To create each type of account, we write :

 val savingsAccount = SavingsAccount(
    accountHolder = "A",
    interestRate = 4f
)

val currentAccount = CurrentAccount(
    accountHolder = "B",
    firmName = "ABC Enterprises",
    firmRegistrationNo = "RJ14-123456"
)

val loanAccount = LoanAccount(
    accountHolder = "C",
    loanAmount = 5_00_000f,
    loanPeriodInMonths = 5 * 12,
    loanInterestRate = 8f
)
 

Advantage

This approach helps us solve the problems faced in common class approach in a cleaner way. We now have separate classes for each accountType, so clear boundaries between all and hence no data inconsistency.

Problems

This solution leads to another major problem - Duplicate Code, No code reusability & Low Maintainability.

Note that the common fields and functions have to defined for each class separately. This is duplicate code and we are unable to reuse the same logic in each class. This leads to Low mainatainability. When adding/modifying/removing a common fields/functionality from BankAccount, we’ll have to make the same changes across all three files. Turns out, this approach also fails.

Inheritance approach

Common functionality & need for code reusability indicate a perfect usecase for Inheritance. In inheritance, we define a Parent class which contains all the logic that is supposed to be re-used.We can then define multiple Child classes that can reuse the logic defined in the parent class.

For the BankAccount example, we define a Parent class BankAccount with all the common fields and functions :

 open class BankAccount(
    val accountHolder: String,
    var balance: Float,
    var isActive: Boolean,
    val openedOn: String,
    val transactions: MutableList<Transaction>
) {
		constructor(account: BankAccount) : this(
        account.accountHolder,
        account.balance,
        account.isActive,
        account.openedOn,
        account.transactions
    )

		fun deposit(amount: Float) { TODO() }
		fun withdraw(amount: Float) { TODO() }
		fun printStatement() { TODO() }
		fun close() { TODO() }
}
 

Note :

  • The use of open keyword makes the class eligible for Inheritance.
  • Each Child class constructor must invoke Parent class constructor for initializing its fields. For this, we have created a Copy Constructor that takes an object of BankAccount and initializes all properties based on that object. We’ll learn more about this topic in the following sections.

Now lets define three Child classes - SavingsAccount, CurrentAccount & LoanAccount all of which inherit from BankAccount class :

 class SavingsAccount(
    account: BankAccount,
    val interestRate: Float
) : BankAccount(account) {

    fun depositInterest() { TODO() }
}
 
 class CurrentAccount(
    account: BankAccount,
    val firmName: String,
    val firmRegistrationNo: String
) : BankAccount(account) {

		fun checkMinimumBalance() { TODO() }
}
 
 class LoanAccount(
    account: BankAccount,
    val loanAmount: Float,
    val loanPeriodInMonths: Int,
    val loanInterestRate: Float,
    var loanAmountPaid: Float = 0f
) : BankAccount(account) {

    fun performLoanRecovery() { TODO() }
}
 

Note :

  • We use : ParentClassName() to define the Parent class
  • Each Child class constructor takes a BankAccount object - account as an argument (not as a field). It is then passed to Parent class constructor for initializing its fields : : BankAccount(account)

To create each type of account, we write :

 val savingsAccount = SavingsAccount(
    BankAccount("A"),
    interestRate = 4f
)

val currentAccount = CurrentAccount(
    BankAccount("B"),
    firmName = "ABC Enterprises",
    firmRegistrationNo = "RJ14-123456"
)

val loanAccount = LoanAccount(
    BankAccount("C"),
    loanAmount = 5_00_000f,
    loanPeriodInMonths = 5 * 12,
    loanInterestRate = 8f
)
 

Note that for creating an object of (say) SavingsAccount, we first create an object of BankAccount (Parent class) and pass it to SavingsAccount constructor.

Advantages

The Inheritance approach overcomes the limitations of not only common class approach but also separate class approach.

  • There is now a clear boundary between each of the accountTypes.
  • There is Code reusability - we define common functionalities only once. And this can be reused multiple times using inheritance.
  • Better Maintainability - Adding, modifying or removing common field/functionality is now easy. We just have make changes in parent class and all child classes automatically adapt to those changes.

Conclusion

We had some common fields and functions that had to be re-used for SavingsAccount, CurrentAccount & LoanAccount. Inheritance provided an intuitive way of reusing code this code across all accountTypes.

Basics

Terminology

The class being inherited is called Parent / Base / Super class. The class inheriting another class is called Child / Derived / Sub class.

Rules

  • A class can inherit from one class only. Multiple Inheritance (inheriting from multiple classes) is not allowed in Kotlin.

Making a class Inheritable

We cannot inherit any random class in Kotlin. All classes are by default final i.e. they can’t be inherited unless explicitly allowed. To make a class inheritable, we must use the open keyword while defining the class :

 open class /* className */

// Example :
open class Employee
 

Inheriting from a class

To inherit from an open class, we use : operator :

 class /* child class name */ : /* parent class name */()

// Example (Manager class inherits from Employee class) :
class Manager: Employee() 
 

Super constructor

Recall the purpose of constructor - to initialize class fields & construct an object. In Inheritance, while creating an object of sub class, object of super class is constructed first i.e. constructor of super class is invoked first. Let’s verify this with the help of an example :

 open class Employee {
    init { println("Employee (Super class) constructor invoked!") }
}

class Manager: Employee() {
    init { println("Manager (Base class) constructor invoked!") }
}

fun main() {
    val manager = Manager()
}

/* Output :

Employee (Super class) constructor invoked!
Manager (Sub class) constructor invoked!
 */
 

Notice the invocation of super (class) constructor on this line :

 class Manager: Employee() {
 

If default values for Super class fields are not defined, they need to be passed by sub class on this line.

 open class Employee(
    val id: Int,
    val name: String
)

class Manager(
    id: Int,
    name: String,
    val subordinates: List<Employee>
): Employee(id, name)

fun main() {
    val e1 = Employee(1, "A")
    val e2 = Employee(2, "B")
    
    val manager = Manager(
        id = 3,
        name = "C",
        subordinates = listOf(e1, e2)
    )
}
 

Note :

  • Employee class (Super) has two fields - id & name
  • Manager class (Sub) has only one field - subordinates and because it inherits from Employee class, it inherently has id & name fields.
  • In Manager class constructor, id & name are normal constructor arguments but subordinates is defined as class property using val. Manager class requires id & name as constructor arguments to pass them into super class constructor - Employee(id, name).

Defining super class fields twice (in super constructor and sub class constructor) might seem an overhead. When adding or removing a field from super class, we have to make changes at both the places. We can solve this problem by using a copy constructor.

Copy Constructor

Copy constructor copies field values from an existing object to create a new one. Example :

 open class Employee(
    val id: Int,
    val name: String
) {
    constructor(employee: Employee): this(
        id = employee.id,
        name = employee.name
    )
}
 

Notice that secondary constructor of Employee class takes an Employee object - employee as an argument and invokes the primary constructor by passing arguments from it.

We can use this constructor to simplify the super constructor invocation in inheritance :

 open class Employee(
    val id: Int,
    val name: String
) {
    constructor(employee: Employee): this(
        id = employee.id,
        name = employee.name
    )
}

class Manager(
    employee: Employee,
    val subordinates: List<Employee>
): Employee(employee)

fun main() {
    val e1 = Employee(1, "A")
    val e2 = Employee(2, "B")

    val manager = Manager(
        Employee(3, "C"),
        subordinates = listOf(e1, e2)
    )
}
 

This way we don’t have to define all fields of super class in sub class. Just one object of super class is sufficient! It can then be passed in copy constructor of super class for initialization.

Overriding functions

We can override (change) the behaviour of a super class function while inheriting. By overriding a function, we can re-define it in the sub class. To do so, we first define the function as open :

 open class /* className */ {
		open fun /* funName */ { }
}

// Example :
open class Animal {
		open fun speak() { 
				println("Animal speaking...")
		}
}
 

Then, to override it (re-define it) in the sub class, we use the override keyword :

 class /* className */: /* superClassName */() {
		override fun /* funName */() { }
}

// Example :
class Dog: Animal() {
		override fun speak() {
				println("Dog barking...")
		}
}
 

The function being overriden must have the exact same signature (name, arguments and return type) as defined in the super class.

When invoking an overriden function, super class function is not invoked :

 open class Animal {
		open fun speak() { 
				println("Animal speaking...")
		}
}

class Dog: Animal() {
		override fun speak() {
				println("Dog barking...")
		}
}

fun main() {
		val animal = Animal()
		animal.speak() // Prints : "Animal speaking..."
		
		val dog = Dog()
		dog.speak() // Prints : "Dog barking..."
}
 

Now that we have overriden Animal#speak() function, its super class definition is not invoked. If we want to invoke the super class function also, we can use the super keyword.

super keyword

super keyword is used to refer to the super class. It can be used to invoke super class constructor and functions. Example :

 open class Animal {
    open fun speak() {
        println("Animal speaking...")
    }
}

class Dog: Animal() {
    override fun speak() {
        println("Dog barking...")
        super.speak()
    }
}

fun main() {
    val dog = Dog()
    dog.speak()
}

/* Output :

Dog barking...
Animal speaking...
 */
 

Note the invocation of Animal#speak() function from Dog#speak() function by super.speak().

Concrete example

In the Inheritance BankAccount example, printStatement() function of all three sub classes (SavingsAccount, CurrentAccount & LoanAccount) is similar except that for LoanAccount, we need to print an extra line in the format :

Loan of Rs. $loanAmount @ $loanInterestPercentage% : EMI of Rs. $emiAmount : $emisPaid / $totalEmis paid

The common functionality of printStatement() can be defined in super class - BankAccount as :

 open class BankAccount(
		/* fields */
) {
		/* functions */

		open fun printStatement() {
        println("\n---- : ACCOUNT STATEMENT for $accountHolder : ----")
        transactions.forEach {
            val type = if (it.amount < 0) "DEBIT" else "CREDIT"
            println("${it.date} -> Rs. ${it.amount} : $type -> ${it.description}")
        }
        println("${getCurrentDate()} -> Rs. $balance -> Closing Balance")
    }
}
 

To override this functionality & print an extra line for LoanAccount, we defined printStatement() function as open. In LoanAccount (sub class,) we override it as follows :

 class LoanAccount(
		/* fields */
): BankAccount(account) {
		/* functions */

		override fun printStatement() {
        println("Loan of Rs. $loanAmount @ $loanInterestRate% : EMI of Rs. ${emiAmount()} : ${progress()}")
        super.printStatement()
    }
}
 

This way we are able to extend the functionality of super class and control it using the super keyword.

Typecasting

Inheritance establishes an is-A relationship between two classes. Suppose Manager class inherits from Employee class then Manager is-A Employee. As a result of this relationship, we can assign instance of derived class to base class variable :

 val /* name */ : /* BaseClass */ = /* DerivedClass */()
 

Example :

 open class Employee {
		fun work() {}
}

class Manager: Employee() {
		fun manage() {}
}

val emp: Employee = Manager() // DerivedClass object in BaseClass variable
 

However, the opposite is not possible i.e. assigning instance of base class to derived class variable is NOT possible :

 val m: Manager = Employee() // ERROR! BaseClass object in DerivedClass variable
 

Once we save derived class instance in base class variable, we won’t be able to access derived class members. We can only access base class members :

 val emp: Employee = Manager()
emp.work() // Okay! Base class member
emp.manage() // ERROR! Derived class member
 

To access derived class members, we need to typecast the base class reference to derived class reference using the as operator.

Typecast refers to referencing an instance of derived class from an instance of base class.

 /* object */ as /* Type to cast to */
 

Example :

 val m = emp as Manager
m.manage() // Okay!

// OR simply :

(emp as Manager).manage() 
 

If the reference is not typecasted correctly, a ClassCastException is thrown at runtime. A cast fails if the object was not instantiated as the type it is being casted to. For example, if we cast an object of Employee class to Manager class, it will fail :

 val empOnly = Employee()
(empOnly as Manager).manage() // Fails, Throws Exception
 

To avoid the exception and safely cast, we can use as? operator. It returns null in case of failure :

 (emp as Manager).manage() // Un-safe cast (May throw Exception)

(emp as? Manager)?.manage() // Safe cast
 

Type checking

To check whether an object is of certain type, we can use the is operator.

 /* object */ is /* Type */   // Returns boolean
 

Example :

 open class Planet
object Earth: Planet()

fun main() {
		println(Earth is Planet) // Prints "true"
}
 

For negation, we can use the Not is operator - !is :

 println(Earth !is Planet) // Prints "false"
 

This operator is useful in when statements :

 open class Planet
object Mercury: Planet()
object Venus: Planet()
object Earth: Planet()
object Mars: Planet()

fun isHabitable(planet: Planet): String {
    return when (planet) {
        is Earth -> "Yes"
        is Mars -> "Intermittently habitable"
        else -> "No"
    }
}
 

protected keyword

Recall that protected is a Visibility modifier. It is similar to private i.e. accessible only inside the class and additionally in the sub-classes also.

Let’s modify the BankAccount program to make it more secure.

 open class BankAccount(
    val accountHolder: String,
    var balance: Float,
    var isActive: Boolean,
    val openedOn: String,
    val transactions: MutableList<Transaction>
) {
		// ...
}
 

Here BankAccount#balance is public i.e. it can easily be modified using the object :

 val savingsAccount = SavingsAccount(
    BankAccount("A"),
    interestRate = 4f
)

savingsAccount.balance = 5_00_000f // Allowed
 

This poses a security risk because data can be accidently modified without a track. We can make it private to avoid this. But then balance can’t be modified in sub-classes also :

 class SavingsAccount(
    account: BankAccount,
    val interestRate: Float
) : BankAccount(account) {

    fun depositInterest() {
				// ... 
				balance += interest // ERROR! balance is private
		}
}
 

In scenarios like this where we need to make class member private but accessible in sub-classes also, we use the protected visibility modifier :

 open class BankAccount(
    val accountHolder: String,
    protected var balance: Float, // Accessible in this class and sub-classes
		// ...
) {
		// ...
}
 

Any class

In Kotlin, all classes implicitly inherit from Any class. It is similar to Object class of Java. The reason behind this is to provide three functions to all classes :

All these functions are defined in Any class. We can easily override them as per our requirements.

Let us understand these functions with the help of some examples.

HashCode

Recall that hashing is used in HashMaps for faster lookups. HashMaps use hashing (set of mathematical operations) to convert an object into an Int (aka hash or HashCode), which is then used to find out index for the HashMap entry. For this purpose, every class in Kotlin is provided with the hashCode() function via the Any class.

TODO : Example - HashSet - similar objects duplicates problem

Printing Objects

So far we have seen how to print a specific property of an object. But what happens if we try to print the object itself? Lets find out!

 class User(
    val id: Int,
    val name: String
)

fun main() {
    val user = User(1, "Ashoka")
    println(user.name) // Prints "Ashoka"
    println(user)      // Prints "User@6e8cf4c6"
}
 

Here we have created a simple class User with two fields - id & name. name is printed correctly but when printing the object itself, we see some strange text - User@6e8cf4c6. Lets decode this.

By default objects of all user defined classes are printed in the format - ClassName@HashCode. HashCode is printed in Hexadecimal format. We can customise this behaviour by overriding Any#toString() and nicely print the User fields instead :

 class User(
    val id: Int,
    val name: String
) {
    override fun toString() = "User#$id : $name"
}

fun main() {
    val user = User(1, "Ashoka")
    println(user) // Prints "User#1 : Ashoka"
}
 

Equality Check

By default, equality checks for objects of user defined classes are reference (address) based. In other words, two objects are considered equal if and only if their addresses match.

 val user = User(1, "Ashoka")
val user1 = User(1, "Ashoka")
 

The above code creates two different objects (with similar values) at two different memory locations (so they have different addresses). Their equality check will result to false :

 println(user == user1) // Prints "false"
 

This might seem strange but following is more strange :

 val user = User(1, "Ashoka")
val users = listOf(
    User(1, "Ashoka")
)

println(users.contains(user)) // Prints "false"
 

Here we created two similar User objects, one is saved in user variable while the other in List - users. The check whether users list contains user object fails. The reason behind this is same as above. List#contains() function relies on equality check which itself is not working as expected. Lets dig deeper and fix this problem.

Equality check for objects use Any#equals() function. Its default behaviour is to check referential equality i.e. whether their addresses are equal. We can customize this behaviour by overriding Any#equals() function based on our requirements :

 class User(
    val id: Int,
    val name: String
) {

    override fun equals(other: Any?): Boolean {
        if (
            /* Reference is equal */
            this === other
        ) return true

        if (
            /* Objects are of different classes */
            javaClass != other?.javaClass
        ) return false

        // Cast other object as User
        other as User

        if (
            /* id is different */
            id != other.id
        ) return false

        if (
            /* name is different */
            name != other.name
        ) return false

        // All fields are equal
        return true
    }
}

fun main() {
    val user = User(1, "Ashoka")
    val user1 = User(1, "Ashoka")

    println(user == user1) // Prints "true"

    val users = listOf(
        User(1, "Ashoka")
    )
    println(users.contains(user)) // Prints "true"
}
 

Note :

  • equals() function is supposed to compare two objects but we receive only one argument - other. Where is first object then? Well, equals() is a member function, so first object is this i.e. current object. In the above example, user is this (first object) and user1 is other (second object) for the equals() function.
  • Referential equality operator === is used in the first if statement. We now know that equality operator == invokes equals() function, but if we want to check only referential equality, we use === operator.
  • Any#equals() function takes other: Any? as an argument i.e. the object we might be comparing to can be null or of another class also. The second if statement checks this.
  • Once we pass the second check, we are sure that the object is of User class so we cast it as User.
  • Here we consider two user objects as equal only if their ids and names match. Alternatively, we can also check id only and consider two user objects as equal if their ids match. We can override this function based on our requirements.

Definition of equals() function might seem complicated. But we need not write it ourselves. We can easily generate this function by right clicking the class → Generate.

Other usecases

Any is the base class of all classes in Kotlin. So, we can use a variable of type Any to store possibly anything!

 val string: Any = "ABCD"
val num: Any = 5
val bool: Any = true
 
  • We can use this property to define a function that can take any input :

     fun beautify(x: Any): String {
        return "---> $x <---"
    }
    
    fun main() {
        val string: Any = "ABCD"
        val num: Any = 5
        val bool: Any = true
    
        println(beautify(string))
        println(beautify(num))
        println(beautify(bool))
    }
     
  • We can define a generic list that can contain sevaral type of values :

     val hybridList: List<Any> = listOf(
        "ABCD", 1.2f, 54, true, "Alpha"
    )
    
    println(hybridList) // Prints "[ABCD, 1.2, 54, true, Alpha]"