Unlock the Power of Kotlin OOP: Master Classes, Inheritance, and Advanced Features

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.

  1. 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)
  1. 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.)

  1. public : default visibility, and makes it visible to any code

  2. private : visible to the containing class only, and can’t be accessed from outside

  3. protected : visible to the containing class & its subclasses, hence it is only applicable inside class & its members

  4. internal : 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 :

interfaceabstract
a class can implement multiple Interfaceshowever, a class can inherit only one abstract class
all methods in interfaces are abstract by default, unless they have some implementationnon-abstract by default unless abstract keyword used.
can not hold statecan hold state
can not have constructor, as they can not hold statecan have constructor
can’t use any another visibility modifier apart from publiccan 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 string

  • ordinal - 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