An Introduction to Kotlin Inheritance and Subclassing
Previous | Table of Contents | Next |
The Basics of Object Oriented Programming in Kotlin | Understanding Android Application and Activity Lifecycles |
In “The Basics of Object Oriented Programming in Kotlin” we covered the basic concepts of object-oriented programming and worked through an example of creating and working with a new class using Kotlin. In that example, our new class was not specifically derived from a base class (though in practice, all Kotlin classes are ultimately derived from the Any class). In this chapter we will provide an introduction to the concepts of subclassing, inheritance and extensions in Kotlin.
Inheritance, Classes and Subclasses
The concept of inheritance brings something of a real-world view to programming. It allows a class to be defined that has a certain set of characteristics (such as methods and properties) and then other classes to be created which are derived from that class. The derived class inherits all of the features of the parent class and typically then adds some features of its own. In fact, all classes in Kotlin are ultimately subclasses of the Any superclass which provides the basic foundation on which all classes are based.
By deriving classes we create what is often referred to as a class hierarchy. The class at the top of the hierarchy is known as the base class or root class and the derived classes as subclasses or child classes. Any number of subclasses may be derived from a class. The class from which a subclass is derived is called the parent class or superclass.
Classes need not only be derived from a root class. For example, a subclass can also inherit from another subclass with the potential to create large and complex class hierarchies.
In Kotlin a subclass can only be derived from a single direct parent class. This is a concept referred to as single inheritance.
Subclassing Syntax
As a safety measure designed to make Kotlin code less prone to error, before a subclass can be derived from a parent class, the parent class must be declared as open. This is achieved by placing the open keyword within the class header:
open class MyParentClass { var myProperty: Int = 0 }
With a simple class of this type, the subclass can be created as follows:
class MySubClass : MyParentClass() { }
For classes containing primary or secondary constructors, the rules for creating a subclass are slightly more complicated. Consider the following parent class which contains a primary constructor:
open class MyParentClass(var myProperty: Int) { }
In order to create a subclass of this class, the subclass declaration references any base class parameters while also initializing the parent class using the following syntax:
class MySubClass(myProperty: Int) : MyParentClass(myProperty) { }
If, on the other hand, the parent class contains one or more secondary constructors, the constructors must also be implemented within the subclass declaration and include a call to the secondary constructors of the parent class, passing through as arguments the values passed to the subclass secondary constructor. When working with subclasses, the parent class can be referenced using the super keyword. A parent class with a secondary constructor might read as follows:
open class MyParentClass { var myProperty: Int = 0 constructor(number: Int) { myProperty = number } }
The code for the corresponding subclass would need to be implemented as follows:
class MySubClass : MyParentClass { constructor(number: Int) : super(number) }
If addition tasks need to be performed within the constructor of the subclass, this can be placed within curly braces after the constructor declaration:
class MySubClass : MyParentClass { constructor(number: Int) : super(number) { // Subclass constructor code here } }
A Kotlin Inheritance Example
As with most programming concepts, the subject of inheritance in Kotlin is perhaps best illustrated with an example. In “The Basics of Object Oriented Programming in Kotlin” we created a class named BankAccount designed to hold a bank account number and corresponding current balance. The BankAccount class contained both properties and methods. A simplified declaration for this class is reproduced below and will be used for the basis of the subclassing example in this chapter:
class BankAccount { var accountNumber = 0 var accountBalance = 0.0 constructor(number: Int, balance: Double) { accountNumber = number accountBalance = balance } open fun displayBalance() { println("Number $accountNumber") println("Current balance is $accountBalance") } }
Though this is a somewhat rudimentary class, it does everything necessary if all you need it to do is store an account number and account balance. Suppose, however, that in addition to the BankAccount class you also needed a class to be used for savings accounts. A savings account will still need to hold an account number and a current balance and methods will still be needed to access that data. One option would be to create an entirely new class, one that duplicates all of the functionality of the BankAccount class together with the new features required by a savings account. A more efficient approach, however, would be to create a new class that is a subclass of the BankAccount class. The new class will then inherit all the features of the BankAccount class but can then be extended to add the additional functionality required by a savings account.
Before a subclass of the BankAccount class can be created, the declaration needs to be modified to declare the class as open:
open class BankAccount { <pre> To create a subclass of BankAccount that we will call SavingsAccount, we simply declare the new class, this time specifying BankAccount as the parent class and add code to call the constructor on the parent class: <pre> class SavingsAccount : BankAccount { constructor(accountNumber: Int, accountBalance: Double) : super(accountNumber, accountBalance) }
Note that although we have yet to add any properties or methods, the class has actually inherited all the methods and properties of the parent BankAccount class. We could, therefore, create an instance of the SavingsAccount class and set variables and call methods in exactly the same way we did with the BankAccount class in previous examples. That said, we haven’t really achieved anything unless we actually take steps to extend the class.
Extending the Functionality of a Subclass
So far we have been able to create a subclass that contains all the functionality of the parent class. In order for this exercise to make sense, however, we now need to extend the subclass so that it has the features we need to make it useful for storing savings account information. To do this, we simply add the properties and methods that provide the new functionality, just as we would for any other class we might wish to create:
class SavingsAccount : BankAccount { var interestRate: Double = 0.0 constructor(accountNumber: Int, accountBalance: Double) : super(accountNumber, accountBalance) fun calculateInterest(): Double { return interestRate * accountBalance } }
Overriding Inherited Methods
When using inheritance it is not unusual to find a method in the parent class that almost does what you need, but requires modification to provide the precise functionality you require. That being said, it is also possible you’ll inherit a method with a name that describes exactly what you want to do, but it actually does not come close to doing what you need. One option in this scenario would be to ignore the inherited method and write a new method with an entirely new name. A better option is to override the inherited method and write a new version of it in the subclass.
Before proceeding with an example, there are three rules that must be obeyed when overriding a method. First, the overriding method in the subclass must take exactly the same number and type of parameters as the overridden method in the parent class. Second, the new method must have the same return type as the parent method. Finally, the original method in the parent class must be declared as open before the compiler will allow it to be overridden. In our BankAccount class we have a method named displayBalance that displays the bank account number and current balance held by an instance of the class. In our SavingsAccount subclass we might also want to output the current interest rate assigned to the account. To achieve this, we simply declare a new version of the displayBalance method in our SavingsAccount subclass, prefixed with the override keyword:
class SavingsAccount : BankAccount { var interestRate: Double = 0.0 constructor(accountNumber: Int, accountBalance: Double) : super(accountNumber, accountBalance) fun calculateInterest(): Double { return interestRate * accountBalance } override fun displayBalance() { println("Number $accountNumber") println("Current balance is $accountBalance") println("Prevailing interest rate is $interestRate") } }
Before this code will compile, the displayBalance method in the BankAccount class must be declared as open:
open fun displayBalance() { println("Number $accountNumber") println("Current balance is $accountBalance") }
It is also possible to make a call to the overridden method in the super class from within a subclass. The displayBalance method of the super class could, for example, be called to display the account number and balance, before the interest rate is displayed, thereby eliminating further code duplication:
override fun displayBalance() { super.displayBalance() println("Prevailing interest rate is $interestRate") }
Adding a Custom Secondary Constructor
As the SavingsAccount class currently stands, it makes a call to the secondary constructor from the parent BankAccount class which was implemented as follows:
constructor(accountNumber: Int, accountBalance: Double) : super(accountNumber, accountBalance)
Clearly this constructor takes the necessary steps to initialize both the account number and balance properties of the class. The SavingsAccount class, however, contains an additional property in the form of the interest rate variable. The SavingsAccount class, therefore, needs its own constructor to ensure that the interestRate property is initialized when instances of the class are created.
Modify the SavingsAccount class one last time to add an additional secondary constructor allowing the interest rate to also be specified when class instances are initialized:
class SavingsAccount : BankAccount { var interestRate: Double = 0.0 constructor(accountNumber: Int, accountBalance: Double) : super(accountNumber, accountBalance) constructor(accountNumber: Int, accountBalance: Double, rate: Double) : super(accountNumber, accountBalance) { interestRate = rate } . . . }
Using the SavingsAccount Class
Now that we have completed work on our SavingsAccount class, the class can be used in some example code in much the same way as the parent BankAccount class:
val savings1 = SavingsAccount(12311, 600.00, 0.07) println(savings1.calculateInterest()) savings1.displayBalance()
Summary
Inheritance extends the concept of object re-use in object oriented programming by allowing new classes to be derived from existing classes, with those new classes subsequently extended to add new functionality. When an existing class provides some, but not all, of the functionality required by the programmer, inheritance allows that class to be used as the basis for a new subclass. The new subclass will inherit all the capabilities of the parent class, but may then be extended to add the missing functionality.
Previous | Table of Contents | Next |
The Basics of Object Oriented Programming in Kotlin | Understanding Android Application and Activity Lifecycles |