Kotlin Training Program

DOWNLOAD APP

FEEDBACK

Abstract class

Abstract class is a type of class which is partially abstract i.e. it contains abstract as well as non-abstract class members.

Interface vs Abstract class

Basics

Defining

We can define an Abstract class using abstract keyword :

 abstract class /* name */ {
		// members 
}
 

Example : For a quiz app that involves 3 types of questions - Multiple Choice, Multiple Select and Boolean, we can define a common structure via Question abstract class -

 abstract class Question(
    val question: String
) {
    abstract fun print()
    abstract fun answerIsCorrect(input: String): Boolean

    fun ask() {
        print()
        print("Enter your answer: ")
        val input = readln()
        println(
            if (answerIsCorrect(input)) "Correct!\n" else "Incorrect!\n"
        )
    }
}
 

Note :

  • Each type of question needs to printed differently and the answer checking mechanism is also different. So, print() and answerIsCorrect() functions are abstract.
  • Abstract functions (functions without definition) have to explicitly declared as abstract
  • ask() function prints the question, prompts for answer input, checks the answer and responds accordingly. This functionality is common across all question types so we have defined it in abstract class itself.
  • Some functions are abstract while one is defined, so this is an example of partially abstract class. Interface can’t be used here because it is fully abstract and does not allow defining a function.

Implementing

We can implement (inherit) from an abstract class the way we inherit from an open class :

 class /* name */ : /* Abstract class name */() {
		// members
}
 

Example : Here is an implementation of Question abstract class defined above -

 class MCQ(
    question: String,
    private val options: List<String>,
    private val answer: String
): Question(question) {

    override fun print() {
        println("Question : $question")
        println("Options (Select one): ")
        options.forEachIndexed { index, option ->
            println("${(index + 'A'.code).toChar()}) $option")
        }
    }

    override fun answerIsCorrect(input: String): Boolean {
        val index = input[0].code - 'A'.code
        return answer == options[index]
    }
}
 

Note :

  • Question class constructor requires question as an argument, so it passed accordingly.
  • MCQ class has two more data members - options and answer
  • print() function prints the question with its options labelled as A, B, C, D.
  • answerIsCorrect() function checks the first character of input by finding the index, looking it up in options, and checking it against the answer.

Ex. - 2D Shapes

Recall the 2D shapes example we discussed in Interface :

 interface Shape {
    fun area(): Int
    fun perimeter(): Int
    fun summary(): String
}

class Square(
    val side: Int
): Shape {

    override fun area() = side * side
    override fun perimeter() = 4 * side

    override fun summary(): String {
        return buildString {
            append("Square")
            append("(side = $side)")
            append(": Area = ${area()}, ")
            append("Perimeter = ${perimeter()}")
        }
    }
}

class Circle(
    val r: Int
): Shape {

    override fun area() = (Math.PI * r * r).toInt()
    override fun perimeter() = (2 * Math.PI * r).toInt()

    override fun summary(): String {
        return buildString {
            append("Circle")
            append("(r = $r)")
            append(": Area = ${area()}, ")
            append("Perimeter = ${perimeter()}")
        }
    }
}
 

Notice that the summary() function is similar in all implementations except the shape name & dimensions. Rest of the code is duplicated. We can further improve this program using Abstract class. summary() function can be lifted up in Shape interface. It can use another new function dimensions() that will return the dimensions string of a shape :

 abstract class Shape {

    abstract fun dimensions(): String
    abstract fun area(): Int
    abstract fun perimeter(): Int

    fun summary(): String {
        return buildString {
            append(this@Shape.javaClass.simpleName)
            append("(${dimensions()})")
            append(": Area = ${area()}, ")
            append("Perimeter = ${perimeter()}")
        }
    }
}
 

Note :

  • We lifted up the summary() function in Shape interface. Because it had to defined, we are now using abstract class. Rest of the functions are abstract.
  • A new function dimensions() is added, which is used by summary() function get the dimensions string of the shape.
  • To get the name of implementing class, we have used the syntax - this@Shape.javaClass.simpleName. It returns the name of class corresponding to this.

Implementing the Shape abstract class :

 class Rectangle(
    val l: Int, val b: Int
): Shape() {
    override fun dimensions() = "l = $l, b = $b"
    override fun area() = l * b
    override fun perimeter() = 2 * (l + b)
}
 

Now that summary() function is lifted up, the code for derived classes is reduced.