Greetings! Today, we're going to demystify the crucial terms of Object-oriented programming (OOP): Inheritance and Polymorphism. These concepts form the backbone of efficient OOP. Our journey will unfold as follows: we'll start with an intuitive grasp of Inheritance and its implementation in Scala, and then we'll delve into Polymorphism, with a clear emphasis on method overriding.
Inheritance is similar to repurposing an old blueprint to create something new. In OOP, it lets one class inherit attributes and methods from another.
In Scala, we have the Parent Class, which offers features, and the Child Class, which receives these features. When implementing, we use the extends
keyword for the Child class to indicate from which Parent class the features are coming.
Scala1// Defining an 'Animal' class (Parent Class) 2class Animal: 3 def eat(): Unit = 4 println("This animal eats food.") // Prints: "This animal eats food." 5 6// 'Cat' class inherits from 'Animal' (Child Class) 7class Cat extends Animal: 8 // Inherits all features from 'Animal' 9 println("Creating an instance of cat...") 10 11@main def run: Unit = 12 val myCat = Cat() 13 myCat.eat() // Prints: "This animal eats food."
Inheritance in Scala involves subclasses inheriting features from their superclass. A significant part of this process is ensuring that constructors in the superclass are adequately invoked by the subclass. Constructors are special methods used to initialize new objects. When a class inherits from another, the subclass must initialize the superclass as well.
When a subclass inherits from a superclass and both have primary constructors, the superclass's constructor automatically gets called when creating an instance of the subclass:
Scala1class Animal(name: String): 2 println(s"$name is an Animal.") 3 4class Cat(name: String) extends Animal(name): 5 println(s"$name is also a Cat.") 6 7@main def run: Unit = 8 val myCat = Cat("Whiskers") 9// Output: 10// Whiskers is an Animal. 11// Whiskers is also a Cat.
In the above example, the Cat
class inherits from Animal
, and both classes have primary constructors that accept a name parameter.
For classes with auxiliary constructors, Scala allows calling another constructor in the same class using the this
keyword, which indirectly ensures the superclass's constructor is called:
Scala1class Animal(val name: String) 2 3class Cat(name: String, lives: Int) extends Animal(name): 4 def this(name: String) = 5 this(name, 9) 6 println(s"$name has $lives lives.") 7 8@main def run: Unit = 9 val myCat = Cat("Felix") 10// Output: Felix has 9 lives.
The power and flexibility of Scala's inheritance mechanism are observable here, where a Cat
class indirectly calls the superclass Animal
constructor through its primary constructor to ensure proper initialization.
Method overriding allows a Child Class to offer unique implementations for methods presented by its Parent Class. In Scala, we use the override
keyword to indicate that a method in the Child Class replaces the method from the Parent Class. It is crucial that the method signatures (the method's name, parameter list, and return type) in the Child Class must be exactly the same as those in the Parent Class for the overriding to work correctly.
Scala1class Animal: 2 def eat(): Unit = 3 println("This animal eats food.") 4 5class Cat extends Animal: 6 override def eat(): Unit = 7 println("The cat eats fish.") // A custom message for the Cat class 8 9@main def run: Unit = 10 val myAnimal = Animal() 11 myAnimal.eat() // Prints: "This animal eats food." 12 13 val myCat = Cat() 14 myCat.eat() // Prints: "The cat eats fish."
When an instance of Animal
and Cat
call their eat()
methods, they print "This animal eats food." and "The cat eats fish." respectively, showcasing how subclasses can customize inherited behavior.
Polymorphism allows objects of different classes to be treated as objects of a common superclass, enabling a method to behave differently according to the object it operates on. This concept, pivotal in Object-oriented Programming (OOP), leverages inheritance and method overriding for dynamic behavior at runtime.
To see polymorphism in action, consider an example with animals and their eating habits:
Scala1class Animal: 2 def eat(): Unit = 3 println("This animal eats food.") 4 5class Cat extends Animal: 6 override def eat(): Unit = 7 println("The cat prefers fish.") 8 9class Dog extends Animal: 10 override def eat(): Unit = 11 println("The dog enjoys bones.") 12 13@main def run: Unit = 14 val animals = Array(Cat(), Dog()) // Array of Animal type 15 animals.foreach(animal => animal.eat()) 16// Output shows specific eating preferences: 17// The cat prefers fish. 18// The dog enjoys bones.
In this example, Cat
and Dog
both override the eat()
method from Animal
. A call to the specific eat()
method for each is made when iterating over an array of Animal objects, illustrating polymorphism: the same operation (eat
) varies by the object type, even when accessed through a reference of the superclass.
So far, so good! We have discovered Inheritance and Polymorphism in Object-oriented programming. We started by understanding Inheritance, which empowers child classes to inherit from parent classes. Then, we looked at method overriding, which allows child classes to provide unique implementations of methods. Lastly, we learned about Polymorphism, which enables multiple behaviors to be realized from a single function.
Now, it's time for some hands-on practice! Practice can reinforce learning and help you become more comfortable with new concepts. Time to roll up our sleeves and dive in!