Skip to main content

Problem Solving with Algorithms and Data Structures using Kotlin The Interactive Edition

Section A.3 Abstract Classes and Interfaces

To complete our discussion of object-oriented programming, we will discuss abstract classes and interfaces.

Subsection A.3.1 Abstract Classes

Consider a group of classes about shapes: The Shape class is the parent of the Rectangle and Circle child classes.
A Shape has an x and y location on the screen, you can calculate its area and perimeter, and you can use toString to convert it to a string. Let’s start writing the code for this class.
Listing A.3.1.
open class Shape(val x: Double, val y: Double) {
    open fun calcArea() : Double {
        // ??????
        return 0.0
    }

    open fun calcPerimeter() : Double {
        // ??????
        return 0.0
    }

    override fun toString() : String {
        return String.format("(%.1f, %.1f)", this.x, this.y)
    }

}
There is a problem rigth away with the calcArea and calcPerimeter methods. How do we calculate the area and perimeter of a “shape”? We don’t have that sort of problem with circles and rectangles. We know the formulas for calculating their area and perimeter (AKA circumference). But a “shape” really doesn’t exist. If I tell you to draw me a shape, you’ll ask me “which one?” Shapes are an abstraction.
Here’s how we’ll solve the problem: we will use the keyword abstract on the class declaration. This tells Kotlin that we will never directly instantiate a Shape object. If anyone tries something like the following, they will get an error message.
val s = Shape(1.0, 2.0) // Error!
We will also put the keyword abstract on the declarations of calcArea and calcPerimeter. And, most important, we will provide only the method header—there will be no method body. Listing A.3.2 shows the new abstract class:
Listing A.3.2.
abstract class Shape(val x: Double, val y: Double) {
    abstract fun calcArea() : Double 

    abstract fun calcPerimeter() : Double 

    override fun toString() : String {
        return String.format("(%.1f, %.1f)", this.x, this.y)
    }

}
Now let’s look at the code for the Circle class, which inherits from Shape. This is not an abstract class—we know how to calculate a circle’s area and perimeter (circumference):
Listing A.3.3. The Circle Class
class Circle(x: Double, y: Double, val radius: Double) : Shape(x, y) {
    override fun calcArea() : Double {
        return Math.PI * radius * radius
    }

    override fun calcPerimeter() : Double {
        return 2 * Math.PI * radius
    }

    fun calcCircumference() : Double {
        return calcPerimeter()
    }

    override fun toString() : String {
        return String.format("Circle: %s [radius %.1f]", super.toString(),
            this.radius)
    }
}
The class can still use the super keyword to call methods in the parent class. Because Shape never defined the body of the calcArea and calcPerimeter methods, the Circle class must override the methods and provide a method body. If we omit a definition for calcArea, we get this error:
error: class 'Circle' is not abstract and does not implement abstract base class member:
fun calcArea(): Double
class Circle(x: Double, y: Double, val radius: Double) : Shape(x, y) {
^^^^^^^^^^^^

Note A.3.4.

Even though you cannot directly instantiate a Shape, you can use polymorphism to create, for example, a MutableList of Shape and assign concrete (non-abstract) objects such as Circle and Rectangle to its elements, as in Listing A.3.5.
Listing A.3.5.
fun main() {
    val shapeList: List<Shape> = listOf(
        Circle(0.0, 0.0, 1.0),
        Rectangle(1.0, 1.0, 2.0, 3.0)
    )
    for (s in shapeList) {
        println(String.format("%s area: %.2f perimeter: %.2f%n",
                s, s.calcArea(), s.calcPerimeter()))
    }
}

Subsection A.3.2 Interfaces

Let’s return to the Bicycle example classes from the inheritance section.
What if we want to have an electric cargo bicycle? We would like to be able to say something like the following:
class ElectricCargoBicycle : ElectricBicycle, CargoBicycle {
    //
}
This is called multiple inheritance
But we can’t! Kotlin does not allow multiple inheritance.
One solution to this problem is to use an interface. It is somewhat like an abstract class, except that it consists of only methods and instance variables (with their associated accessors and mutators). Listing A.3.6 shows the Electrified interface:
Listing A.3.6.
interface Electrified {
    val chargeCapacity: Int
    var currentCharge: Double
}
Let’s look at the Bicycle class in Listing A.3.7:
Listing A.3.7.
open class Bicycle(val frameSize: Double, val nGears: Int) {
    var currentGear: Int = nGears
    set(value) {
        minOf(nGears, maxOf(1, value))
    }
}
Now, our ElectricBicycle class lookss like this:
Listing A.3.8.
class ElectricBicycle(frameSize: Double, nGears: Int, override val chargeCapacity: Int) : Bicycle(frameSize, nGears), Electrified {
    override var currentCharge: Double = 0.0
        set(value) {
            field = maxOf(0.0, minOf(chargeCapacity.toDouble(), value))
        }
}
Line 1 is the key here: the class implements the interface. Because this class implements Electrified, it must override the instance variables required by the interface, and it can customize the mutator as usual.
We can now create our ElectricCargoBicycle by using an interface. We extend CargoBicycle and implement Electrified:
Listing A.3.9.
class ElectricCargoBicycle(frameSize: Double, nGears: Int, maxLoad: Double, override val chargeCapacity: Int) : CargoBicycle(frameSize, nGears, maxLoad), Electrified {
    override var currentCharge: Double = 0.0
        set(value) {
            field = maxOf(0.0, minOf(chargeCapacity.toDouble(), value))
        }

    }

Note A.3.10.

In this particular example, we could also use composition to create a Battery class and put one of those objects into the electric bicycle and electric cargo bicycle classes.
Kotlin makes extensive use of interfaces in much of its class library. For example, the MutableList class implements these interfaces:
  • Iterable, which provides methods for enabling iteration via enhanced for loops
  • MutableCollection, which provides methods for adding and removing elements
  • List, which provides methods for integer indexing
You have attempted of activities on this page.