Polymorphism in Scala
In the previous chapter, we learned how inheritance allows one class to reuse and extend another class.
Now imagine you're building an Employee Management System with three types of employees:
Developer
Manager
Tester
All three employees perform work, but their responsibilities are different.
A developer writes code, a manager manages a team, and a tester tests applications.
Wouldn't it be useful if we could call the same method, work(), and let each employee perform their own task?
That's exactly what polymorphism allows us to do.
What Is Polymorphism?
Polymorphism means many forms.
In object-oriented programming, polymorphism allows the same method or interface to behave differently depending on the object or arguments involved.
Consider this example:
class Employee {
def work(): Unit = {
println("Employee is performing general work")
}
}
class Developer extends Employee {
override def work(): Unit = {
println("Developer is writing code")
}
}
class Manager extends Employee {
override def work(): Unit = {
println("Manager is managing the team")
}
}Now create objects and call the same method:
object Main extends App {
val developer = new Developer()
val manager = new Manager()
developer.work()
manager.work()
}Output:
Developer is writing code
Manager is managing the team
Both objects call work(), but each produces a different result.
This is polymorphism in action.
Types of Polymorphism in Scala
For practical OOP development, three important forms are:
Subtype polymorphism
Method overloading
Parametric polymorphism
Let's understand each one.
Subtype Polymorphism (Runtime Polymorphism)
Subtype polymorphism allows a parent class reference to hold an object of its child class.
The actual object's overridden method is selected at runtime.
Let's use our Employee example.
class Employee(val name: String) {
def work(): Unit = {
println(s"$name is performing general work")
}
}
class Developer(name: String) extends Employee(name) {
override def work(): Unit = {
println(s"$name is writing code")
}
}
class Manager(name: String) extends Employee(name) {
override def work(): Unit = {
println(s"$name is managing the team")
}
}
object Main extends App {
val employees: List[Employee] = List(
new Developer("Rahul"), new Manager("Priya"), new Employee("Amit")
)
employees.foreach(_.work())
}Output:
Rahul is writing code
Priya is managing the team
Amit is performing general work
Look at this declaration:
val employees: List[Employee] = List(
new Developer("Rahul"), new Manager("Priya"), new Employee("Amit")
)The collection is declared as
List[Employee], but it contains different employee types.
When work() is called, Scala executes the implementation belonging to the actual object.
This is called runtime method dispatch or dynamic dispatch.
Why is this useful?
Imagine adding another employee type called Tester.
You can create a new Tester class and override work() without changing the existing employee-processing logic.
That's one of the biggest advantages of polymorphism.
Method Overloading (Compile-Time Polymorphism)
Method overloading means defining multiple methods with the same name but different parameter lists.
Scala determines which method to call based on the arguments supplied at compile time.
For example:
class Calculator {
def add(a: Int, b: Int): Int = { a + b }
def add(a: Double, b: Double): Double = { a + b }
def add(a: Int, b: Int, c: Int): Int = { a + b + c }}
object Main extends App {
val calculator = new Calculator()
println(calculator.add(10, 20))
println(calculator.add(10.5, 20.5))
println(calculator.add(10, 20, 30))
}Output:
30
31.0
60
Notice that all three methods have the same name: add.
But their parameter lists are different:
Two integer parameters
Two double parameters
Three integer parameters
Scala selects the matching method based on the arguments.
Important: Changing only the return type is not sufficient to overload a method. The parameter list must differ.
Method Overriding vs Method Overloading
These two concepts are often confused, but they solve different problems.
| Method Overriding | Method Overloading |
|---|---|
| Usually involves inheritance | Can happen within one class |
| Subclass provides a new implementation | Multiple methods share the same name |
| Same method signature | Different parameter lists |
| Runtime method dispatch | Compile-time method selection |
Uses override | Does not require override |
A simple way to remember:
Overriding: Same method, different implementation.
Overloading: Same method name, different parameters.
Parametric Polymorphism (Generics)
Parametric polymorphism allows you to write reusable code that works with different data types.
Scala supports this through generics.
Consider a method that returns the first element of a list.
Without generics, you might end up writing separate methods for integers, strings, and other types.
Instead, use a type parameter.
object Main {
def firstElement[A](items: List[A]): A = {
items.head
}
def main(args: Array[String]): Unit = {
val numbers = List(10, 20, 30)
val names = List("Rahul", "Priya", "Amit")
println(firstElement(numbers))
println(firstElement(names))
}
}Output:
10
Rahul
Here, [A] is a generic type parameter.
The method can work with different types:
List[Int]List[String]List[Double]
Scala infers the type from the supplied argument.
This makes the method reusable without duplicating its implementation.
Polymorphism with Abstract Classes
Abstract classes are another way to design polymorphic behavior.
An abstract class can define methods without providing their implementation.
Subclasses must provide the missing implementation unless they are also abstract.
abstract class Payment {
def processPayment(amount: Double): Unit
}
class CreditCardPayment extends Payment {
override def processPayment(amount: Double): Unit = {
println(s"Processing credit card payment: $amount")
}
}
class UpiPayment extends Payment {
override def processPayment(amount: Double): Unit = {
println(s"Processing UPI payment: $amount")
}
}
object Main extends App {
val payments: List[Payment] = List(
new CreditCardPayment(),
new UpiPayment()
)
payments.foreach(_.processPayment(1000))
}Output:
Processing credit card payment: 1000.0
Processing UPI payment: 1000.0
The application works with the common Payment type without needing to know the specific payment implementation at every call site.
This approach is particularly useful when building extensible systems.
For example, you can add a new payment method without changing the code that processes a collection of payments.
Real-World Example: Payment Processing System
Let's connect polymorphism to a real application.
Imagine an e-commerce platform that supports:
Credit card payments
UPI payments
Net banking payments
All payment methods perform a payment operation, but each uses a different implementation.
Instead of writing separate processing logic for every payment type, define a common abstraction.
abstract class Payment {
def pay(amount: Double): Unit
}
class CreditCard extends Payment {
override def pay(amount: Double): Unit = {
println(s"Paid $amount using Credit Card")
}
}
class UPI extends Payment {
override def pay(amount: Double): Unit = {
println(s"Paid $amount using UPI")
}
}
class NetBanking extends Payment {
override def pay(amount: Double): Unit = {
println(s"Paid $amount using Net Banking")
}
}
object Main extends App {
def processPayment(payment: Payment, amount: Double): Unit = {
payment.pay(amount)
}
processPayment(new CreditCard(), 2500)
processPayment(new UPI(), 1500)
processPayment(new NetBanking(), 3000)
}Output:
Paid 2500.0 using Credit Card
Paid 1500.0 using UPI
Paid 3000.0 using Net Banking
The processPayment() method accepts the common Payment type.
It doesn't need separate methods for credit cards, UPI, or net banking.
This makes the application easier to extend and maintain.
In a production payment system, each implementation would also handle validation, provider integration, transaction status, retries, and error handling.
When Should You Use Polymorphism?
Polymorphism is especially useful when:
Multiple classes share a common contract but behave differently.
You want to add new implementations without modifying existing processing logic.
You need reusable methods that work with different data types.
You want to avoid large conditional blocks based on object types.
You are designing extensible systems such as payment processing, notification services, or employee management.
Don't introduce inheritance or polymorphism unnecessarily. Use them when they make the design easier to extend and understand.
Remember: Write code that depends on a common abstraction rather than a specific implementation whenever that makes the design simpler.
