Kotlin – 15 – Polymorphism in Kotlin

Polymorphism is a core concept in object-oriented programming that allows objects of different classes to be treated as objects of a common superclass. It enables you to write more flexible and extensible code by providing a unified interface to work with various objects. Kotlin fully supports polymorphism, and in this guide, we’ll explore the concept of polymorphism in Kotlin, its practical applications, and how to implement it effectively.

Polymorphism Basics

At its core, polymorphism means “many shapes.” In the context of programming, it refers to the ability of different classes to be treated as instances of a common superclass. This enables you to write code that works with objects in a more abstract and general way, without needing to know the specific details of each object’s class.

Polymorphism is typically achieved through method overriding, where a subclass provides its own implementation of a method that is already defined in its superclass. When you call a method on an object, the runtime system determines which version of the method to execute based on the actual class of the object, allowing for different behavior depending on the object’s type.

Method Overriding

In Kotlin, method overriding is straightforward. To override a method in a subclass, you use the override keyword before the method declaration, ensuring that the method in the subclass has the same name, return type, and parameter list as the one in the superclass. Here’s an example:

open class Shape {
    open fun area(): Double {
        return 0.0
    }
}

class Circle(val radius: Double) : Shape() {
    override fun area(): Double {
        return Math.PI * radius * radius
    }
}

class Square(val sideLength: Double) : Shape() {
    override fun area(): Double {
        return sideLength * sideLength
    }
}

In this example, the Shape class defines an area() method, and both Circle and Square subclasses override this method to provide their own implementations. When you call area() on an object of type Shape, the specific version of the method defined in the object’s class (either Circle or Square) is executed.

The is and as Operators

Two essential operators in Kotlin for working with polymorphism are the is and as operators.

The is operator checks if an object is an instance of a particular class or type. It returns true if the object is an instance of the specified class or a subclass and false otherwise. Here’s an example:

val shape: Shape = Circle(5.0)
if (shape is Circle) {
    println("It's a Circle")
}

The as operator is used for casting an object to a specific class or type. It allows you to treat an object as an instance of a particular class temporarily. If the cast is not valid, it may throw a ClassCastException. Here’s an example:

val shape: Shape = Circle(5.0)
val circle = shape as Circle // Valid cast
Using Polymorphism

Polymorphism is incredibly useful when you want to work with objects in a generic way, without being concerned about their specific implementations. For example, you can create a function that calculates and prints the area of any shape, regardless of whether it’s a circle, square, or any other shape:

fun printArea(shape: Shape) {
    println("Area: ${shape.area()}")
}

fun main() {
    val circle = Circle(5.0)
    val square = Square(4.0)

    printArea(circle) // Output: Area: 78.53981633974483
    printArea(square) // Output: Area: 16.0
}

In this code, the printArea function takes a Shape object as its parameter, and it doesn’t need to know the specific class of the object. It relies on polymorphism to call the appropriate area() method based on the actual type of the object.

Abstract Classes and Interfaces

To further leverage polymorphism, you can use abstract classes and interfaces in Kotlin. An abstract class can define abstract (unimplemented) methods that must be overridden by its subclasses. Similarly, interfaces can declare methods that implementing classes must provide. This allows you to define a contract that multiple classes can adhere to, promoting polymorphism.

abstract class Shape {
    abstract fun area(): Double
}

class Circle(val radius: Double) : Shape() {
    override fun area(): Double {
        return Math.PI * radius * radius
    }
}

class Square(val sideLength: Double) : Shape() {
    override fun area(): Double {
        return sideLength * sideLength
    }
}

fun main() {
    val circle = Circle(5.0)
    val square = Square(4.0)

    val shapes: List<Shape> = listOf(circle, square)
    for (shape in shapes) {
        println("Area: ${shape.area()}")
    }
}

In this example, the Shape class is abstract and declares an abstract method area(). Both Circle and Square subclasses override this method. In the main() function, we create a list of Shape objects and iterate through them, demonstrating polymorphism by calling the area() method on each object.

Command and Example

Here’s a complete example demonstrating polymorphism in Kotlin:

open class Shape {
    open fun area(): Double {
        return 0.0
    }
}

class Circle(val radius: Double) : Shape() {
    override fun area(): Double {
        return Math.PI * radius * radius
    }
}

class Square(val sideLength: Double) : Shape() {
    override fun area(): Double {
        return sideLength * sideLength
    }
}

fun printArea(shape: Shape) {
    println("Area: ${shape.area()}")
}

fun main() {
    val circle = Circle(5.0)
    val square = Square(4.0)

    printArea(circle) // Output: Area: 78.53981633974483
    printArea(square) // Output: Area: 16.0
}

In this example, we have a superclass Shape with an area() method and two subclasses, Circle and Square, that override the area() method to provide their own implementations. The printArea() function demonstrates polymorphism by accepting Shape objects and printing their areas, showcasing how objects of different classes can be treated uniformly.