Lesson 5
Introduction to Polymorphism in Kotlin
Introduction to Polymorphism

Welcome back! We're continuing our journey into object-oriented programming (OOP) with a new and exciting topic: Polymorphism. You've already learned about classes, objects, and inheritance, which are essential building blocks of OOP. Now it's time to explore how polymorphism can make your code more flexible and reusable.

Polymorphism in Kotlin

Polymorphism allows objects to be treated as instances of their parent class rather than their actual class. This provides a way to use a single interface to represent different types of objects. Here’s what we’ll cover in this lesson:

  1. Understanding Polymorphism: We'll discuss what polymorphism is and why it's a powerful concept in Kotlin programming.
  2. Runtime Polymorphism: You'll learn how to use interfaces and method overriding to achieve runtime polymorphism.
  3. Compile-Time Polymorphism: You'll understand method overloading and how it's used for compile-time polymorphism.
Understanding Polymorphism

Polymorphism in Kotlin allows you to call derived class methods through a base class or interface reference. This can make your code more dynamic and general. Polymorphism in Kotlin can be achieved through two distinct methods:

  • Method Overriding (Runtime Polymorphism): This is when a subclass provides a specific implementation of a method that is already defined in its superclass. In Kotlin, you use the open modifier for methods in the superclass that you want to allow to be overridden, and you use the override modifier in the subclass.
  • Method Overloading (Compile-Time Polymorphism): This is when multiple methods in the same class have the same name but different parameters. They may have different return types, but using different parameter lists allows Kotlin to distinguish between them.

Polymorphism enables a single action to be performed in different ways. The ability to redefine functions in derived classes and have a unified method call mechanism via base classes or interfaces is a strength of Kotlin OOP.

Runtime Polymorphism

Runtime polymorphism is achieved through method overriding. Here are the rules for method overriding:

  1. Same Method Signature: The function in the derived class must have the same name, return type, and parameters as the function in the base class.
  2. Inheritance: The class must be a subclass of the base class where the original function is defined.
  3. Access Modifier: The overriding function cannot have a more restrictive access modifier than the function in the base class.
  4. Invocation: The Kotlin runtime determines which function to invoke based on the object's actual class, not the reference type.
  5. Annotations: It's a good practice to use the @Override annotation to ensure that you are indeed overriding a method.

Example:

Kotlin
1open class Animal { 2 open fun sound() { 3 println("Animal makes a sound") 4 } 5} 6 7class Dog : Animal() { 8 override fun sound() { 9 println("Dog barks") 10 } 11} 12 13class Cat : Animal() { 14 override fun sound() { 15 println("Cat meows") 16 } 17} 18 19fun main() { 20 val myDog: Animal = Dog() 21 myDog.sound() // Output: Dog barks 22 23 val myCat: Animal = Cat() 24 myCat.sound() // Output: Cat meows 25}

In this example, the sound function in Dog and Cat overrides the sound function in Animal. When sound is called on the Animal reference pointing to a Dog or Cat object, the respective sound function of Dog or Cat is invoked.

Compile-Time Polymorphism

Compile-time polymorphism is achieved through method overloading. Here are the rules for method overloading:

  1. Same Method Name: The functions must have the same name but different parameter lists.
  2. Different Parameters: The parameter lists must differ in type, number, or both. This is how the compiler distinguishes between the overloaded functions.
  3. Return Type: The return type can be different or the same; however, the return type alone is not sufficient to distinguish overloaded functions.
  4. Access Modifier: The access modifier can be different for overloaded functions.
  5. Default Arguments: Kotlin provides default arguments, which can be used as an alternative to some overloading scenarios.

Example:

Kotlin
1class MathOperation { 2 fun add(a: Int, b: Int): Int { 3 return a + b 4 } 5 6 fun add(a: Double, b: Double): Double { 7 return a + b 8 } 9} 10 11fun main() { 12 val math = MathOperation() 13 println(math.add(2, 3)) // Output: 5 14 println(math.add(2.5, 3.5)) // Output: 6.0 15}

In this example, the add function is overloaded. The compiler determines which add function to call based on the type of the arguments.

Why It Matters

Polymorphism is crucial because it introduces flexibility and scalability to your code:

  • Code Flexibility: Polymorphism allows you to write functions that can operate on objects of different types through a common interface.
  • Reusability: You can extend and reuse your code more efficiently by leveraging polymorphism.
  • Simplified Code Management: Polymorphism helps you manage and understand your code better, as similar operations are handled in a unified manner.

Now that you've grasped the concepts of polymorphism, it's time to put your knowledge to the test. Proceed to the practice section for some hands-on practice exercises!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.