Unlock the Power of Kotlin OOP: Master Classes, Inheritance, and Advanced Features
Dive into Kotlin OOP: From Beginner Concepts to Advanced Mastery
Object Oriented Programming is a programming methodology that involves execution with the help of classes and objects.
Introduction
Class - Blueprint of an object, class has properties(data members) and member methods(behaviour to that data)
Object - Instance of a class
Classes in Kotlin are declared using the class
keyword :
class Employee{
//properties - data members
var name : String = ""
var id : Int = 0
//member methods
fun sayHello(){
print("Hi" + name)
}
}
//need to initialize class properties during class
//declaration or make it abstract
Whenever we create an object, it gets associated with its following data members & member functions
val emp1 = Employee() //emp1 is the object name
//now using the object we can access it data members & functions
emp1.name = "Vedant"
emp1.sayHello() //Hi Vedant
Constructor
Constructor is basically a member function of our class, which is by default called when the object of a class is created. This is done to initialise the data members.
- Primary Constructor
Primary Constructor is declared in the class header. It initialises the object and the data members declared in the class header. For executing any runnable code with the Constructor, it should be placed inside the init block.
class Student(var name: String, var id: Int){
//statements..
}
class Student constructor(var name:String, var id: Int){
//statements..
}
class Student(var name: String, var id: Int){
init{ print(name) }
init{ print(id) }
} //init block executed in the order of their declaration
class Student(var name: String, var id: Int = 0){
//statements..
}
val student1 = Student("Vedant", 19)
- Secondary Constructor
Secondary Constructor is defined inside the class body & more than one secondary constructor can be present in a class (based on function overloading)
class Car{
var carModel : String
var carColor : String
constructor(cm : string, carColor : string){
carModel = cm
this.carColor = carColor
//this refers to the class variable, and used when the
//class variable name is same as the constructor parameters name
print(carModel + carColor)
//can even execute staements in the secondary constructor block
}
}
val car1 = Car("ABC", "DEF") //ABCDEF
Inheritance
Classes in Kotlin can acquire properties and behaviour of another class, in such a case the class that acquires is the child class, and the higher class is the parent class. By default classes in Kotlin are final
, which means they can’t be inherited. To inherit from a class it should be declared open
.
open class Car{ //parent class
var numType = 4
fun display{ print(numTyre) }
}
class bmwModel : Car(){ //child class inheriting from parent
var logo = "Circle"
fun display{ print(logo) }
}
class mercedezModel(var numCar: Int) : Car(){
//statements
}
//Instantiation
var bmwCar = bmwModel()
print(bmwCar.numTyre) //4
print(bmwCar.logo) //Circle
When there are methods/properties in child class which are same as the parent class , we need to use open
and override
keywords in the parent and child class respectively.
open class Shape {
open fun draw() { /* ... */ }
}
class Square() : Shape {
override fun draw() { /* ... */ }
super.draw() //calls draw function of the parent class
}
Getters & Setters in Kotlin
Getter and Setter are auto generated functions in Kotlin which are used to get and set values to data members, we can also create our own implementation of the function for the same.
If we implement a custom getter & setter for a property, they will be called every time we access the property or set the property. Thus, val
doesn’t allow a custom setter and a setter is not executed when the property is initialised.
class User{
var userId : String = "default"
get(){
//statements
return field
}
set(value){
//statements
field = value
}
}
//default - auto generated get and set implementation :
get() = field
set(value) { field = value }
class User{
var userId : Int = 0
set(value){
if(value > 100) return
field = value
}
}
val user = User()
print(user.userId) //0
user.userId = 101
print(user.userId) //0
user.userId = 42
print(user.userId) //42
Visibility Modifiers
Visibility modifiers help us in controlling the visibility of class and its members(object, interfaces, constructors, functions, properties, setters, etc.)
public
: default visibility, and makes it visible to any codeprivate
: visible to the containing class only, and can’t be accessed from outsideprotected
: visible to the containing class & its subclasses, hence it is only applicable inside class & its membersinternal
: visible only to the particular module
// File: example.kt
// in Module : myModule
public fun one() { }
private fun two() { }
internal fun three() { }
// File: another.kt
// in Module : myModule
fun accessOne() {
// one() - can access, as it is public
// two() - cant access 'two', as it is private
// three() - can access, as it is internal
}
// File: another.kt
// in Module : anotherModule
import myModule.one
fun accessOne() {
// one() - can access after importing, as it is present in another module
// two() - can't access 'two', as it is private
// three() - can't access
}
open class User {
fun display() { }
private val name = "Vedant"
protected fun pf() { }
protected open fun pf2() { }
}
class Child : User() {
//pf() - Allowed
//we can override the properties visibility modifier
override public fun pf2() { }
}
fun outsideAccess() {
val user = User()
// user.name - Not Allowed
// user.pf() - Not Allowed
user.display()
val child = Child()
// child.name - Not Allowed
// child.pf - Not Allowed
child.display()
child.pf2()
}
Abstract Classes
Abstract refers to incompleteness, a class can be declared abstract
if it has some abstract methods. Abstract members do not have an implementation as they are incomplete.
We can’t instantiate an abstract class, i.e. can’t create an object of it, till it is complete. To achieve so, we first need to complete the abstract methods by extending it using a subclass.
abstract class Animal{
fun eat(){ /* .. */ }
abstract val num : Int
abstract fun speak()
}
class Cat : Animal(){
override val num = 4
override fun speak() { }
//no need to declare function open if abstract
}
fun main(){
val anim = Animal()
anim.eat()
//anim.num - Error
//anim.speak() - Error
val c = Cat()
c.eat()
print(c.num)
c.speak()
}
Interfaces
Interfaces are similar to Abstract Classes in Kotlin. However certain functionalities set Interfaces aside. Interfaces are declared using the interface
keyword.
Interfaces can contain both abstract methods, non-abstract methods, and abstract data members. It can not contain variables with values that will lead it into a blocking state. However, we can implement getters and setters for variables that are accessed. Getters will help return a computed value to the accessor without the variable actually storing it & Setter can compute, without setting a value in the variable.
There is no need to declare functions abstract
in Interfaces.
abstract class myAbstract{
val num : Int = 4
}
interface myInterface{
val name : String //abstract data
val desc : String //abstract data with getter
get() = "fsafsa"
val logMessage : String //abstract data with getter
get() = "dsads"
set(value){
print("sdawfa $value")
}
fun a() //abstract method
fun b() { /* */ } //non-abstract methods
}
class imp : myInterface, myAbstract{
override val name = "Vedant"
override fun a() { /* */ }
}
fun main(){
val ob = imp()
print(ob.name)
ob.a()
ob.b()
}
Difference between abstract methods and interfaces clearly explained :
interface | abstract |
a class can implement multiple Interfaces | however, a class can inherit only one abstract class |
all methods in interfaces are abstract by default, unless they have some implementation | non-abstract by default unless abstract keyword used. |
can not hold state | can hold state |
can not have constructor, as they can not hold state | can have constructor |
can’t use any another visibility modifier apart from public | can use any visibility modifier |
Interfaces can Derive from other Interfaces.
Interface a{
//statements
}
Interface b : a{
//statements
}
Data Classes
Data classes in Kotlin are special classes that are used to hold data, as they provide us with some additional member functions like, display, copy and compare instance. These inbuilt functions are only accessible to the data members defined inside the primary constructor of the class, and not the data members declared inside the class body.
Data classes are created using the keyword data
. Data class can’t be abstract, open, sealed or inner and have at least one parameter.
data class Person(val name: String, val age: Int){
var id : Int = 0
}
val person1 = Person("A", 19)
val person2 = Person("B", 21)
val person3 = person1.copy()
val person4 = person1.copy(name = "C")
//copy with change in properties
print(person1 == person2) //compare instance -> false
person3.id = 10
print(person1 == person3) //true -> as not applicable on id
print(person1) //Person(name="A", age=19)
//deconstructing a declaration :
val (name1, age1) = person1
print(name1) //A
Sealed Classes & Interfaces
Sealed classes & interfaces provide controlled inheritance to classes by allowing only subclasses from the same module. This also helps in covering the behaviour of all possible subclasses using a when
expression.
sealed class Shape{
//creating subclasses way 1
class Point : Shape()
data class Circle(var radius : Float) : Shape()
}
//creating subclasses way 2
class Rectangle(var l : Int, var b: Int) : Shape()
fun main(){
var c = Shape.Circle(3f)
var r = Rectangle(3,1)
checkShape(c)
}
fun checkShape(shape : Shape){
when(shape){
is Shape.Point -> //statements..
is Shape.Circle -> //statements..
is Rectangle -> //statements..
else -> //statements..
}
}
Sealed classes have multiple use-cases ranging from state management in ui applications, payment method handling, api handling, etc. Sealed classes are abstract
and hence can’t be instantiated. Being abstract makes the functioning of sealed class possible, and hence we generally don’t need to override the sealed class. Example of overriding a sealed class members :
sealed class Shape {
abstract fun area(): Double // Abstract method
fun description(): String = "I am a shape"
}
class Circle(val radius: Double) : Shape() {
override fun area(): Double = Math.PI * radius * radius
}
class Rectangle(val length: Double, val breadth: Double) : Shape() {
override fun area(): Double = length * breadth
}
Nested & Inner Classes
Classes & Interfaces can be nested in each other . A nested classes can be marked Inner by using the inner
keyword which enables it to access the members of its outer class.
class outerInterface {
private val bar : Int = 1
class innerClass1{
fun returnTwo() = 2
}
inner class innerClass2{
fun returnBar() = bar
}
interface InnerInterface
}
val demo1 = outerInterface.innerClass1().returnTwo() //2
val demo2 = outerInterface.innerClass2().returnBar() //1
Enum Classes
Enum classes are special classes that are used to represent a fixed set of constants or values. Each constant in an enum class is an object that can have its own properties, methods, or custom behaviour. Enum classes are especially useful when you need to represent a predefined set of options, such as directions, days of the week, or states.
Default Properties :
name
- name of the constant as a stringordinal
- index of the constant
Default Methods :
values()
- returns an array of all enum constants
enum class direc1 {
NORTH, SOUTH, EAST, WEST
}
enum class direc2(val degrees: Int) {
NORTH(0),
EAST(90),
SOUTH(180),
WEST(270);
fun description() = "$name is $degrees degrees"
}
fun main() {
val d = direc2.NORTH
println(d) // NORTH
println(d.ordinal) // 0
println(d.name) // NORTH
println(d.degrees) // 0
println(d.description()) // NORTH is 0 degrees
// Loop through all enum values
for (dir in Direction.values()) {
println(dir)
}
}
Object Declaration & Expression
Object Declaration is used to create Singleton class(a class that has only one instance, helpful when only one instance is a appreciated, eg: managing database connection). The rest functionality like Inheritance, etc. of an Singleton class remains the same as a normal class.
// Declares a Singleton object to manage data providers
object dataProviderManager {
private val providers = mutableListOf<DataProvider>()
// Registers a new data provider
fun registerDataProvider(provider: DataProvider) {
providers.add(provider)
}
// Retrieves all registered data providers
val allDataProviders: Collection<DataProvider>
get() = providers
}
//Using Object
dataProvideManager.registerDataProvider(exampleProvider)
Data Objects are just singleton data classes, used to represent a singleton instance with data-like properties. These are similar to data classes, except that they are singleton and don’t support the copy()
method.
data object Singleton {
val name = "abc"
val id = 1
}
print(Singleton.name) //abc
Companion Object is a special type of object declared inside a class, which allows us to create data members and functions that are accessible even without creating an instance of the class. However, a class can have only one companion object
.
class MyClass {
companion object {
val con = "This is a constant"
fun greet() = "Hello from the companion object!"
}
fun useCon() = con
}
fun main() {
//no need to create on object of the class
println(MyClass.con)
println(MyClass.greet())
val myclass = MyClass()
println(myClass.useCon())
}
Object Expression helps us create anonymous objects that is declaring a class and creating its instance as an expression without naming either of them.
val sayHi = object{
val greet = "hi"
fun say() = greet
}
print(sayHi) //hi