Polymorphism is a core concept in object-oriented programming (OOP) that allows objects to be treated as instances of their parent class rather than their actual class. This enables a single interface to represent different underlying forms (data types). In Groovy, polymorphism is achieved through method overriding and interface implementation.

Key Concepts

  1. Method Overriding: Subclasses provide a specific implementation of a method that is already defined in its superclass.
  2. Interface Implementation: Different classes can implement the same interface, allowing them to be used interchangeably.

Method Overriding

Method overriding allows a subclass to provide a specific implementation of a method that is already defined in its superclass. This is useful when the subclass needs to modify or extend the behavior of the superclass method.

Example

class Animal {
    void makeSound() {
        println "Some generic animal sound"
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        println "Bark"
    }
}

class Cat extends Animal {
    @Override
    void makeSound() {
        println "Meow"
    }
}

Animal myDog = new Dog()
Animal myCat = new Cat()

myDog.makeSound() // Output: Bark
myCat.makeSound() // Output: Meow

Explanation

  • Animal Class: The superclass with a method makeSound().
  • Dog and Cat Classes: Subclasses that override the makeSound() method to provide specific implementations.
  • Polymorphic Behavior: The myDog and myCat variables are of type Animal, but they call the overridden methods in Dog and Cat classes, respectively.

Interface Implementation

Interfaces define methods that must be implemented by any class that implements the interface. This allows different classes to be used interchangeably if they implement the same interface.

Example

interface Shape {
    double area()
}

class Circle implements Shape {
    double radius

    Circle(double radius) {
        this.radius = radius
    }

    @Override
    double area() {
        return Math.PI * radius * radius
    }
}

class Rectangle implements Shape {
    double width
    double height

    Rectangle(double width, double height) {
        this.width = width
        this.height = height
    }

    @Override
    double area() {
        return width * height
    }
}

Shape myCircle = new Circle(5)
Shape myRectangle = new Rectangle(4, 6)

println "Circle area: ${myCircle.area()}" // Output: Circle area: 78.53981633974483
println "Rectangle area: ${myRectangle.area()}" // Output: Rectangle area: 24.0

Explanation

  • Shape Interface: Defines a method area().
  • Circle and Rectangle Classes: Implement the Shape interface and provide specific implementations of the area() method.
  • Polymorphic Behavior: The myCircle and myRectangle variables are of type Shape, but they call the implemented area() methods in Circle and Rectangle classes, respectively.

Practical Exercise

Exercise

Create a superclass Vehicle with a method move(). Then, create two subclasses Car and Bicycle that override the move() method. Instantiate objects of Car and Bicycle and call the move() method on each.

Solution

class Vehicle {
    void move() {
        println "The vehicle is moving"
    }
}

class Car extends Vehicle {
    @Override
    void move() {
        println "The car is driving"
    }
}

class Bicycle extends Vehicle {
    @Override
    void move() {
        println "The bicycle is pedaling"
    }
}

Vehicle myCar = new Car()
Vehicle myBicycle = new Bicycle()

myCar.move() // Output: The car is driving
myBicycle.move() // Output: The bicycle is pedaling

Explanation

  • Vehicle Class: The superclass with a method move().
  • Car and Bicycle Classes: Subclasses that override the move() method to provide specific implementations.
  • Polymorphic Behavior: The myCar and myBicycle variables are of type Vehicle, but they call the overridden methods in Car and Bicycle classes, respectively.

Common Mistakes and Tips

  • Forgetting to Use @Override Annotation: Always use the @Override annotation when overriding methods to avoid mistakes and improve code readability.
  • Incorrect Method Signatures: Ensure that the method signature in the subclass matches the method signature in the superclass or interface.
  • Not Leveraging Polymorphism: Use polymorphism to write more flexible and reusable code. For example, you can create a list of Vehicle objects that can contain both Car and Bicycle objects.

Conclusion

Polymorphism is a powerful feature in Groovy that allows for flexible and reusable code. By understanding and utilizing method overriding and interface implementation, you can create more dynamic and maintainable applications. In the next section, we will explore encapsulation, another fundamental concept in object-oriented programming.

© Copyright 2024. All rights reserved