Method Overriding
Method overriding is a fundamental concept in object-oriented programming that allows a subclass to provide a specific implementation for a method that is already defined in its superclass. When a method in a subclass has the same name, same parameters, and same return type (or a subtype) as a method in its superclass, the method in the subclass is said to override the method in the superclass.
This is a key mechanism for achieving polymorphism in Java.
Rules for Method Overriding
- Method Signature: Must have the same method name and the same parameter list (number, type, and order of parameters). This requires an "IS-A" relationship (inheritance).
- Return Type: The return type must be the same or a covariant type (a subtype of the original return type).
- Access Modifier: Cannot be more restrictive than the overridden method's modifier (e.g., you cannot override a
publicmethod with aprivateone). finalandstatic:finalmethods cannot be overridden.staticmethods cannot be overridden (they are hidden, not overridden).
The @Override Annotation
It is a best practice to use the @Override annotation above a method that you intend to override. This tells the compiler your intention. If the method doesn't correctly override a superclass method (e.g., due to a typo in the method name), the compiler will generate an error, saving you from potential bugs.
Example 1: Overriding toString()
Every class in Java implicitly extends the Object class. The Object class has a toString() method that, by default, prints the class name and its identity hash code. This is often not very useful. We can override it to provide a more meaningful string representation of our object.
// Without overriding toString()
class Book {
private String title;
private String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
}
Book book1 = new Book("1984", "George Orwell");
// Default behavior from Object class
System.out.println(book1); // Prints something like: Book@2f92e0f4
Now, let's provide a better implementation by overriding toString().
class BookWithOverride {
private String title;
private String author;
public BookWithOverride(String title, String author) {
this.title = title;
this.author = author;
}
@Override
public String toString() {
return "Book[title=" + this.title + ", author=" + this.author + "]";
}
}
BookWithOverride book2 = new BookWithOverride("Dune", "Frank Herbert");
// Calls our overridden method
System.out.println(book2); // Prints: Book[title=Dune, author=Frank Herbert]
Example 2: A Vehicle Hierarchy
Consider a general Vehicle class with a method to describe its movement. Different types of vehicles move in different ways.
class Vehicle {
public void move() {
System.out.println("The vehicle moves.");
}
}
class Car extends Vehicle {
@Override
public void move() {
System.out.println("The car drives on the road.");
}
}
class Boat extends Vehicle {
@Override
public void move() {
System.out.println("The boat sails on the water.");
}
}
public class TestVehicles {
public static void main(String[] args) {
Vehicle myVehicle = new Vehicle();
Vehicle myCar = new Car();
Vehicle myBoat = new Boat();
myVehicle.move(); // Prints: The vehicle moves.
myCar.move(); // Prints: The car drives on the road.
myBoat.move(); // Prints: The boat sails on the water.
}
}
In this example, even though myCar and myBoat are referenced by Vehicle type variables, the Java Virtual Machine (JVM) executes the method from the actual object's class (Car or Boat) at runtime.
Example 3: Calculating Employee Bonuses
Imagine a company where different types of employees get bonuses calculated in different ways. We can have a base Employee class and subclasses that override the bonus calculation.
class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public double getSalary() {
return salary;
}
// Default bonus calculation
public double calculateBonus() {
return this.salary * 0.10; // 10% base bonus
}
}
class Manager extends Employee {
public Manager(String name, double salary) {
super(name, salary);
}
@Override
public double calculateBonus() {
// Managers get a 20% bonus
return getSalary() * 0.20;
}
}
class Engineer extends Employee {
public Engineer(String name, double salary) {
super(name, salary);
}
@Override
public double calculateBonus() {
// Engineers get a 15% bonus
return getSalary() * 0.15;
}
}
This design allows us to treat all employees uniformly when we need to, while still applying the specialized logic from each subclass when calculating their specific bonuses.