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.