Kotlin Training Program

DOWNLOAD APP

FEEDBACK

Class & Object

Class is user defined data type which can encapsulate (contain) related data & functions.

Let us understand the need of class with the help of some examples :

Examples

Ex1 : Students Grade

Lets write a program that calculates grades obtained by some students. Each student has a rollNo, name & marks (list of marks in 5 subjects). Given this data of some students, we need to print rollNo. name -> grade for each student.

Example

Input

 RollNo, Name, Marks
11, "Aman", [10, 9, 8, 10, 6]
20, "Yash", [5, 6, 7, 7, 8]
31, "John", [8, 8, 8, 10, 9]
50, "Juliet", [7, 7, 8, 6, 8]
 

Output

 11. Aman -> A
20. Yash -> C
31. John -> A
50. Juliet -> B
 

Normal approach

We need to save data of multiple students where each student has rollNo, name & marks. rollNo will have type Int, so we need List<Int> to save rollNums of all students. Similarly we need List<String> for names and List<List<Int>> for marks :

 val rollNums = listOf(11, 20, 31, 50)
val names = listOf("Aman", "Yash", "John", "Juliet")
val marks = listOf(
    listOf(10, 9, 8, 10, 6),
    listOf(5, 6, 7, 7, 8),
    listOf(8, 8, 8, 10, 9),
    listOf(7, 7, 8, 6, 8),
)
 

Now, lets calculate the average marks of each student :

 val averageMarks = marks.map { it.average() }
 

To map average marks to grades, lets define a function gradeFor(marks: Double): String which will return the grade for given marks :

 fun gradeFor(marks: Double): String {
    return when (marks) {
        in 9.0..10.0 -> "A+"
        in 8.0..9.0 -> "A"
        in 7.0..8.0 -> "B"
        in 6.0..7.0 -> "C"
        in 5.0..6.0 -> "D"
        else -> "E"
    }
}
 

Finally, we use this function to calculate grades of each student :

 val grades = averageMarks.map { gradeFor(it) }
 

Now, we can print rollNo. name -> grade for each student :

 repeat(rollNums.size) {
    println("${rollNums[it]}. ${names[it]} -> ${grades[it]}")
}
 

Note that we have used separate lists to save each field / attribute of Student entity: rollNums, names, marks. If there are more fields, we’ll need more lists.

Object Oriented approach

Recall that Class is a user defined data type. In this example, Student is a user defined data type. Related to it, it has some data / attributes - rollNo, name and marks.

Also, it has two related functions :

  • grade() which calculates & returns the grade of the student.
  • printWithGrade() which prints the student info in the required format

The concept of Class enables us to encapsulate (group) this data and functions into a single entity - Student class like this :

 class Student(
    val rollNo: Int,
    val name: String,
    val marks: List<Int>
) {
    fun grade(): String {
        return when (marks.average()) {
            in 9.0..10.0 -> "A+"
            in 8.0..9.0 -> "A"
            in 7.0..8.0 -> "B"
            in 6.0..7.0 -> "C"
            in 5.0..6.0 -> "D"
            else -> "E"
        }
    }

    fun printWithGrade() {
        println("$rollNo. $name -> ${grade()}")
    }
}
 

This is only the definition of Student class and serves as a blueprint. To use this blueprint and actually save something, we need Objects. We create an object of the student class like this :

 val aman = Student(11, "Aman", listOf(10, 9, 8, 10, 6))
 

So, to save information of multiple students, we now need only one list :

 val students = listOf(
    Student(11, "Aman", listOf(10, 9, 8, 10, 6)),
    Student(20, "Yash", listOf(5, 6, 7, 7, 8)),
    Student(31, "John", listOf(8, 8, 8, 10, 9)),
    Student(50, "Juliet", listOf(7, 7, 8, 6, 8))
)
 

Finally, to print students info with their grades, we write :

 students.forEach { it.printWithGrade() }
 

Comparison

Encapsulation

In the Normal approach, we needed to save the data of multiple students in separate lists. But in Object Oriented approach, we encapsulated the fields and defined the Student class. This way, we need only one list - List<Student>.

Addionally, we were able to define the functions related to Student (grade() & printWithGrade()) within the same class.

Avoiding errors

While saving data in multiple lists, we can’t gaurantee that all lists will be of same size. For example, below code leads to error :

 val rollNums = listOf(11, 20, 31, 50)
val names = listOf("Aman", "Yash")

repeat(rollNums.size) {
    println("${rollNums[it]}. ${names[it]}")
}
 

Size of rollNums is 4 but that of names is only 2. When printing with size as 4, we get ArrayIndexOutOfBoundsException when trying to access names[2] and the program crashes.

Such error is easily avoided when following Object Oriented approach because each Student object is sure to have all fields - rollNo, name & marks.

Conclusion

Object Oriented approach helps us write code in a cleaner & organized way. We can define classes in separate files and maintain large codebase in an efficient manner.

Ex2 : Income Tax Calculator

Lets write a program to calculate income tax based on two different regimes (income tax slabs) of multiple persons given their name & income. The taxes of each regime should be printed along with person details in the format : $personName (Income = $income) → $tax1 vs $tax2.

Example

Tax Regimes

Regime 1 Tax Slabs
Income Tax percentage
below Rs. 3L exempted
Rs. 3L - 6L 5%
Rs. 6L - 9L 10%
Rs. 9L - 12L 15%
Rs. 12L - 15L 20%
Rs. above 15L 30%
Regime 2 Tax Slabs
Income Tax percentage
below Rs. 2.5L exempted
Rs. 2.5L - 5L 5%
Rs. 5L - 7.5L 10%
Rs. 7.5L - 10L 15%
Rs. 10L - 12.5L 20%
Rs. 12.5L - 15L 25%
above Rs. 15L 30%
  • Note that L refers to Lakhs (100,000) and Rs. refers to the currency INR

Input

 Person, IncomeInLakhRupees
A, 20
B, 12
C, 7
D, 2
 

Output

 A  (Income = 20.0L) -> Tax = 3.0L vs 3.375L
B  (Income = 12.0L) -> Tax = 0.9L vs 1.15L
C  (Income = 7.0L) -> Tax = 0.25L vs 0.325L
D  (Income = 2.0L) -> Tax = 0.0L vs 0.0L
 

Tax Calculation

Following is an example of how tax is calculated for an income of Rs. 7L based on Regime 1 :

Income Tax percentage Taxable amount in slab Tax
below Rs. 3L exempted 3L (left = 4L) 0
Rs. 3L - 6L 5% 3L (left = 1L) 15000
Rs. 6L - 9L 10% 1L 10000
Rs. 9L - 12L 15% Nil
Rs. 12L - 15L 20% Nil
Rs. above 15L 30% Nil
Total Tax 25000 (0.25L)

Normal approach

Lets first see how to save each tax slab described with its lower limit, upper limit & tax percentage. We can use a Pair<Pair<Float, Float>, Int> as (lower limit → upper limit) → taxPercentage :

 // For tax slab 3L - 6L (5%) :
val slab = (3f to 6f) to 5
 

Then, to represent a tax regime, we create list or map of such pairs :

 val taxRegime = mapOf(
    (0f to 3f) to 0,
    (3f to 6f) to 5,
    (6f to 9f) to 10,
    (9f to 12f) to 15,
    (12f to 15f) to 20,
    (15f to Float.MAX_VALUE) to 30
)
 

To save multiple regimes, we create a list of regime :

 val taxRegimes = listOf(
    mapOf(
        (0f to 3f) to 0,
        (3f to 6f) to 5,
        (6f to 9f) to 10,
        (9f to 12f) to 15,
        (12f to 15f) to 20,
        (15f to Float.MAX_VALUE) to 30
    ),
    mapOf(
        (0f to 2.5f) to 0,
        (2.5f to 5f) to 5,
        (5f to 7.5f) to 10,
        (7.5f to 10f) to 15,
        (10f to 12.5f) to 20,
        (12.5f to 15f) to 25,
        (15f to Float.MAX_VALUE) to 30,
    )
)
 

Now, lets focus on tax calculation function :

 fun calculateTax(regime: Map<Pair<Float, Float>, Int>, incomeInLakhs: Float): Float {
    var incomeLeft = incomeInLakhs
    var tax = 0f
    for (slab in regime) {
        val taxable = minOf(incomeLeft, slab.key.second - slab.key.first)
        tax += taxable * slab.value / 100
        incomeLeft -= taxable
        if (incomeLeft == 0f) break
    }
    return tax
}
 
  • We simply iterate over each slab until the incomeLeft > 0
  • For each slab,
    • taxable amount is calculated as minOf(incomeLeft, slab size) & deducted from the incomeLeft
    • tax is calculated and added

To prepare the tax comparison text as $tax1 vs $tax2, we define another function :

 fun getTaxComparison(incomeInLakhs: Float): String {
    return taxRegimes.map { regime -> calculateTax(regime, incomeInLakhs) }
        .joinToString(" vs ") { "${it}L" }
}
 

To save details of persons, we need two lists - one for name (List<String>) and another for their incomes (List<Float>) :

 val personNames = listOf("A", "B", "C", "D")
val personIncomesInLakhs = listOf(20f, 12f, 7f, 2f)
 

Finally, we iterate over the list and calculate & print tax comparison :

 repeat(personNames.size) {
    println("${personNames[it]}  (Income = ${personIncomesInLakhs[it]}L) -> Tax = ${getTaxComparison(personIncomesInLakhs[it])}")
}
 

Object Oriented approach

The entities that can be defined as user defined data type / Class are TaxSlab, TaxRegime and Person. Lets define a class for each of these first.

TaxSlab has 3 data fields - lowerLimit, upperLimit and taxPercentage. Also, we can define a function - size() for it, which will return the size of the slab :

 class TaxSlab(
    val lowerLimit: Float,
    val upperLimit: Float,
    val taxPercentage: Int
) {
    fun size() = upperLimit - lowerLimit
}
 

A TaxSlab object can then be defined as :

 val taxSlab = TaxSlab(0f, 3f,  0)
 

TaxRegime has a single data field slabs: List<TaxSlab> and can have a function calculateTax() :

 class TaxRegime(
    val slabs: List<TaxSlab>
) {
    fun calculateTax(incomeInLakhs: Float): Float {
        var incomeLeft = incomeInLakhs
        var tax = 0f
        for (slab in slabs) {
            val taxable = minOf(incomeLeft, slab.size())
            tax += taxable * slab.taxPercentage / 100
            incomeLeft -= taxable
            if (incomeLeft == 0f) break
        }
        return tax
    }
}
 

Multiple taxRegimes can then be saved in a List<TaxRegime> :

 private val taxRegimes = listOf(
    TaxRegime(
        listOf(
            TaxSlab(0f, 3f,  0),
            TaxSlab(3f, 6f,  5),
            TaxSlab(6f, 9f,  10),
            TaxSlab(9f, 12f,  15),
            TaxSlab(12f, 15f, 20),
            TaxSlab(15f, Float.MAX_VALUE, 30)
        )
    ),
    TaxRegime(
        listOf(
            TaxSlab(0f, 2.5f,  0),
            TaxSlab(2.5f, 5f,  5),
            TaxSlab(5f, 7.5f,  10),
            TaxSlab(7.5f, 10f,  15),
            TaxSlab(10f, 12.5f, 20),
            TaxSlab(12.5f, 15f, 25),
            TaxSlab(15f, Float.MAX_VALUE, 30)
        )
    )
)
 

Person entity has two data fields - name & incomeInLakhs. Also, we can define a function printTaxComparison() to calculate and print the tax comparison :

 class Person(
    val name: String,
    val incomeInLakhs: Float
) {
    fun printTaxComparison() {
        val taxes = taxRegimes.map { it.calculateTax(incomeInLakhs) }
            .joinToString(" vs ") { "${it}L" }
        println("$name (Income = ${incomeInLakhs}L) -> Tax = $taxes")
    }
}
 

Persons info can then be saved in a List<Person> :

 val persons = listOf(
    Person("A", 20f),
    Person("B", 12f),
    Person("C", 7f),
    Person("D", 2f),
)
 

Finally, we iterate over the persons list and invoke the printTaxComparison() function :

 persons.forEach { it.printTaxComparison() }
 

Comparison

Encapsulation

In the Normal approach, we needed to save the data of multiple persons in separate lists. But in Object Oriented approach, we encapsulated the fields and defined the Person class. This way, we need only one list - List<Person>.

Addionally, we were able to define the function related to Person - printTaxComparison() within the same class.

Code readability

In Normal approach, we defined a slab as Pair<Pair<Float, Float>, Int> and a tax regime as list of such pairs. So, to access its parameters in tax calculation, we wrote :

 val taxable = minOf(incomeLeft, slab.key.second - slab.key.first)
tax += taxable * slab.value / 100
 

Note that the use of slab.key.second, slab.key.first & slab.value decrease code readability.

But when we followed Object oriented approach, we were able to use TaxSlab object as :

 val taxable = minOf(incomeLeft, slab.size())
tax += taxable * slab.taxPercentage / 100
 

This improved code readability!

Avoiding errors

Similar to Ex1, Object oriented approach helps us avoid the ArrayIndexOutOfBoundsException.

Conclusion

Object Oriented approach helps us write code in a cleaner, readable and organized way. We can define classes in separate files and maintain large codebase in an efficient manner.

Examples from Kotlin StdLib

Classes are almost everywhere in Kotlin. All primitive data types like Int, Float, String etc are Classes. Creating a variable of these types creates an object of that class :

 val x = 5     // Creates an Int object
val y = "abc" // Creates a String object
 

These primitive data type classes provide many useful functions like String.uppercase(), String.substring(), Int.coerceAtMost(), Int.countOneBits() etc :

 println(y.uppercase()) // ABC
println(y.substring(1)) // bc
println(x.coerceAtMost(3)) // 3
println(x.countOneBits()) // 2
 

Collections like List, Map, Set etc. are also classes. Using functions like listOf(), mapOf(), setOf(), we create an object of such collection classes :

 val list = listOf(1, 2, 3) // Creates an object of List<Int>
val map = mapOf("A+" to 10) // Creates an object of Map<String, Int>
val set = setOf('A', 'B') // Creates an object of Set<Char>
 

Note that List, Map and Set are Generic classes. Ex. - we are able to define List<Int>, List<String> etc. using the same List<T> class where T can be any class.

Defining a Class

We use the class keyword to define a class. A class can optionally have multiple data members (aka data fields or properties or state) & member functions (aka behaviors) :

 class /* className */(
		/* data members (optional) */
) {
		/* member functions (optional) */
}
 

Examples :

 // Simple class with no data members or member functions
class Planet

// Class with single data member
class Person(val name: String)

// Class with 2 data members
class Student(
		val rollNo: Int,
		val name: String
)

// Class with 2 data members & 1 member function
class Cart(
		val items: List<Item>,
		var discount: Int
) {
		fun total() = items.sumOf { it.amount } - discount
}

fun main() {}
 

Note that we can use both val and var to define data members of a class.

Constructing an Object

A class is just a blueprint and doesn’t hold any data. We cannot access Class members directly. Following code doesn’t compile :

 class Person(val name: String)

fun main() {
		println(Person.name) // ERROR, Doesn't store anything!
}
 

To actually create an instance / object of the class, we invoke the constructor of class.

Instance / Object of a class is a variable of type class.

Constructor is a special function of a class used to conctruct (create) an instance (object) of that class. → It’s name is the class name. → It is also used to initialize the fields of class.

In the Planet class defined below, notice how the data members are enclosed in parenthesis (). Using this syntax, we define the constructor for this class which takes just one argument - name :

 class Planet(val name: String)
 

To create an object of Planet class, we invoke this constructor function using the class name :

 val earth = Planet("Earth")
 

Here Person is just a class & earth is an instance (object) of that class. Notice that we used the constructor to initialize the name field of Person class.

If we don’t define a constructor, one constructor is provided by default :

 class Star

val sun = Star()
 

Accessing class members

We use the dot . notation to access members (data members & member functions) of a class :

 class Rectangle(val l: Int, val b: Int) {
		fun area() = l * b
		fun perimeter() = 2 * (l + b)
}

fun main() {
		val r1 = Rectangle(2, 6)
		println("l = ${r1.l}, b = ${r1.b}")
		println("area = ${r1.area()}, perimeter = ${r1.perimeter()}")
}
 

Class vs Object

Lets understand the difference between Class and Object with the help of some examples :

init block

If we want to execute a piece of code when the object is constructed, we can use the init (initializer) block :

 class /* className */(
		/* dataMembers */
) {
		init {
				/* code */
		}

		/* memberFunctions */
}
 

Example :

 class Planet(val name: String) {
    init {
        println("constructing Planet $name...")
    }
}

fun main() {
    val earth = Planet("Earth") // Prints "constructing Planet Earth..."
}
 

Constructor Parameters

One thing that might seem odd in constructor function is that we can define its arguments using val & var keywords (Special function remember?). But function arguments are implicitly val only and can’t be explicitly defined as var or val :

 fun square(val x: Int) = x * x    // val not allowed, won't work!
fun cube(var x: Int) = x * x * x  // var not allowed, won't work!

// But this works, how?
class Planet(var name: String)
 

val & var in constructor are used to define the data member of the class and not just a function argument. Further it indicates whether the data member is mutable or not.

 class Student(
		val rollNo: Int,
		var name: String
) 

fun main() {
		val s1 = Student(1, "A")
		s1.rollNo = 5  // val cannot be reassigned!
		s1.name = "A1" // Works!
}
 

If we remove the val or var keyword, the parameter acts as an argument to the constructor and not data member of the class.

Consider the following example - Lets define a User class with one data member name but this name should be created from given firstName, middleName and lastName. We can do it like this :

 class User(
    firstName: String,
    middleName: String,
    lastName: String
) {
    val name: String

    init {
        name = listOf(firstName, middleName, lastName)
            .filter { it.isNotBlank() }
            .joinToString(" ")
    }
}

fun main() {
    val users = listOf(
        User("A", "", "C"),
        User("B", "P", "Q"),
        User("C", "R", "")
    )
    users.forEach {
        // println(it.firstName) -> Won't work!
        println(it.name) // Works!
    }
}
 

Note :

Polymorphism & Secondary Constructor

The constructor defined with class name is called the Primary constructor. Apart from that, we can define multiple Secondary constructor also.

 class /* className */(
		/* dataMembers */
) {
		constructor(/* params */): this(/* initialize data members */) {
				/* code */
		}

		/* memberFunctions */
}
 

Example

Lets define a class Planet with one data member - name. The primary constructor takes name as an argument. Lets define a secondary constructor which takes no arguments and initializes name data member with the value Unknown and prints Identified Unknown planet....

 class Planet(val name: String) {
    constructor(): this("Unknown") {
        println("Identified Unknown planet...")
    }
}

fun main() {
    val earth = Planet("Earth")
    val unknown = Planet() // Prints "Identified Unknown planet..."
}
 

Every constructor is invoked using class name so using the same name we are able to do different things. The Secondary Constructor feature is an example of Polymorphism.

Polymorphism is a feature of OOPs. It literally translates to “many forms”. It allows us to define multiple forms of constructor.

Constructor Default Argument

Recall that a function can be defined with default values for its arguments. Because constructor is also a function, we can define default values for constructor parameters & data members also :

 class /* className */(
		/* arg / dataMember */ = /* default value */
) {

		/* memberFunctions */
}
 

Example

Lets define a class Square used for drawing a Square on screen. It has 3 data members with default values - size (100), color (Black), strokeWidth (1).

 class Sqaure(
    val size: Int = 100,
    val color: String = "Black",
    val strokeWidth: Int = 1
)

fun main() {
    val s1 = Sqaure() // All default values assigned
    val s2 = Sqaure(color = "Red") // Default color & strokeWidth
    val s3 = Sqaure(color = "Red", strokeWidth = 2) // Default size
    val s4 = Sqaure(size = 200, color = "Red", strokeWidth = 2) // All custom values 
}
 

By leveraging Default argument feature of Kotlin, we are able to achieve Polymorphism (many forms of constructor) without defining secondary constructors. This reduces boilerplate code & increases readability.

Visibility Modifiers

Visibility modifiers are keywords that help us control the accessibility of class & class members. Ex. - public, private, protected, internal

All classes and their members are public by default i.e. we can access them anywhere in our project. We can control the accessibility using certain visibility modifiers. But when do we need to do so? We need to control accessibility in order to achieve abstraction (hide implementation details) & enhance data security (prevent accidental modification of data).

We have the following visibility modifiers :

Lets see the power of these visibility modifiers with the help of an example :

 // File BankAccount.kt -----------------------------

// Private class - accessible only in current file
private class BankAccount(
    val acHolderName: String,
    balance: Int,
    private var pin: String
) {
    var balance: Int 
    private set  // Private setter, value can be re-assigned only inside the class

    init {
        this.balance = balance
    }
    
    fun close() { TODO() }
    
    private fun depositInterest() { TODO() }
}

fun main() {
    val ac1 = BankAccount("A", 500, "7654")
    
    println(ac1.acHolderName)   // Accessible because its public
    
    println(ac1.pin)    // ERROR! pin is private
    ac1.balance = 5     // ERROR! setter is private
    
    ac1.close()     // Accessible because its public
    ac1.depositInterest()   // ERROR! function is private
}

// File BankAccountUtils.kt --------------------------

fun printStatement(
    account: BankAccount // ERROR! BankAccount is a private class
) {
    // Code...    
}