Module lessons (2/2)
Polymorphism and Abstract Classes
Polymorphism and abstract classes are advanced object-oriented programming concepts that allow us to write flexible, extensible code independent of specific implementations.
Polymorphism
The term polymorphism (many forms) refers to the ability to treat objects of different classes as if they belong to a common type. In Java, a superclass reference variable can store a reference to an object of any of its subclasses.
class Animal {
public void makeSound() {
System.out.println("Verso...");
}
}
class Dog extends Animal {
public void makeSound() {
System.out.println("Woof");
}
}
class Cat extends Animal {
public void makeSound() {
System.out.println("Miao");
}
}
Thanks to polymorphism, we can do this:
Animal myAnimal1 = new Dog(); // Polymorphism
Animal myAnimal2 = new Cat(); // Polymorphism
myAnimal1.makeSound(); // Executes Dog's method (Woof)
myAnimal2.makeSound(); // Executes Cat's method (Miao)
The decision on which method to invoke is made at runtime (Late Binding or dynamic dispatch) based on the actual object, not the type of the reference variable.
Abstract Classes (abstract)
An abstract class is a class marked with the abstract keyword that cannot be directly instantiated (you cannot do new MyAbstractClass()). It serves as a "partial template" for other classes.
Abstract Methods
An abstract class can contain abstract methods: methods declared without a body (no curly braces and no code), ending with a semicolon. Non-abstract subclasses are required to implement all inherited abstract methods.
abstract class Shape {
String color;
// Abstract method (no body)
public abstract double getArea();
}
class Circle extends Shape {
double radius;
public Circle(double radius) {
this.radius = radius;
}
// Required to implement getArea
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
Dynamic Binding / Late Binding
How does Java know which method to execute at runtime? This process is known as Late Binding (or Dynamic Binding). Unlike overloading (which is resolved at compile-time), the compiler does not know which specific method will be executed. Instead, it generates a generic call instruction. At runtime, the Java Virtual Machine (JVM) inspects the actual object in memory and invokes the method implementation defined in that object's class.
Abstract Classes and Missing Implementation
If a subclass inherits from an abstract class, it has two options:
- Implement all abstract methods: in this case, the subclass can be a normal (concrete) class and can be instantiated.
- Declare itself abstract: if the subclass does not provide an implementation for all inherited abstract methods, it must also be declared with the
abstractkeyword.
abstract class Animal {
public abstract void makeSound();
}
// This class MUST be abstract because it does not implement makeSound()
abstract class Canine extends Animal {
// Inherits makeSound() but does not implement it
}
Try it yourself
Declare a variable s of type Shape and assign a new Circle object to it using polymorphism.
Show hint
Write `Shape s = new Circle();` to use the base type as the reference type.
Solution available after 3 attempts
Make the Shape class abstract and add the abstract method double getArea(). Then complete Square so that it extends Shape and implements getArea() returning side * side.
Show hint
Declare `public abstract double getArea();` in Shape. In Square, add `@Override public double getArea() { return side * side; }`.
Solution available after 3 attempts
Declare in the main method an array of type Shape[] containing a Circle object with radius 2.0 and a Square object with side 3.0. Then, use a loop (for or for-each) to print the area of each shape by invoking the getArea() method.
Show hint
Declare the array with `Shape[] shapes = { new Circle(2.0), new Square(3.0) };` and loop through it using `for (Shape s : shapes)` printing `s.getArea()`.
Solution available after 3 attempts