Method Overriding in Java
Understand how child classes customize parent methods to achieve runtime polymorphism and flexible designs.
What is Method Overriding?
Method overriding happens when a subclass (child) provides its own specific implementation of a method that already exists in its superclass (parent). The child class essentially replaces the parent's version with its own custom behavior while keeping the method signature identical.
Imagine a parent teaching all children to "introduce yourself." Each child follows the same instruction but introduces themselves differently based on their personality. Same action, different execution.
- Requires inheritance relationship between classes
- Child method must match parent's method signature exactly
- Enables runtime polymorphism (dynamic method dispatch)
- Provides specialized behavior while maintaining a common interface
💡 Quick Distinction: Overloading = same class, different parameters. Overriding = parent-child relationship, identical signature, different behavior.
Basic Overriding Example
See how different animal types override the same method to produce unique behaviors.
Example: Animal Sound Override
// Parent class
class Animal {
void makeSound() {
System.out.println("Some generic animal sound");
}
}
// Child class 1
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof! Woof!");
}
}
// Child class 2
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Meow! Meow!");
}
}
// Child class 3
class Cow extends Animal {
@Override
void makeSound() {
System.out.println("Moo! Moo!");
}
}
public class OverrideDemo {
public static void main(String[] args) {
Animal myDog = new Dog();
Animal myCat = new Cat();
Animal myCow = new Cow();
myDog.makeSound(); // Woof! Woof!
myCat.makeSound(); // Meow! Meow!
myCow.makeSound(); // Moo! Moo!
}
}
Woof! Woof! Meow! Meow! Moo! Moo!
Explanation:
- Each animal class extends Animal and overrides makeSound()
@Overrideannotation ensures we're correctly overriding (optional but recommended)- All animals share the same method name but execute different code
- Parent reference can hold child objects (polymorphism)
Runtime Polymorphism in Action
The true power of overriding: Java decides which method to execute at runtime based on the actual object type, not the reference type.
Example: Dynamic Method Dispatch
class Shape {
void draw() {
System.out.println("Drawing a shape");
}
void area() {
System.out.println("Calculating area");
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("Drawing a circle ⭕");
}
@Override
void area() {
System.out.println("Area = π × r²");
}
}
class Rectangle extends Shape {
@Override
void draw() {
System.out.println("Drawing a rectangle ▭");
}
@Override
void area() {
System.out.println("Area = length × width");
}
}
public class PolymorphismExample {
public static void main(String[] args) {
// Parent reference, child objects
Shape shape1 = new Circle();
Shape shape2 = new Rectangle();
Shape shape3 = new Shape();
// Runtime decides which method to call
shape1.draw(); // Circle's draw()
shape1.area(); // Circle's area()
shape2.draw(); // Rectangle's draw()
shape2.area(); // Rectangle's area()
shape3.draw(); // Shape's draw()
shape3.area(); // Shape's area()
}
}
Drawing a circle ⭕ Area = π × r² Drawing a rectangle ▭ Area = length × width Drawing a shape Calculating area
Explanation:
- Reference type is Shape, but actual objects are Circle and Rectangle
- JVM determines the actual object type during runtime (not compile time)
- Method called depends on object type, not reference type
- This flexibility is the foundation of polymorphic behavior
Essential Rules for Method Overriding
1. Exact Method Signature
Method name, parameter list, and return type must be identical to the parent method.
Rule: Even one parameter difference makes it overloading, not overriding
2. Inheritance is Mandatory
Only possible when child class extends parent class (IS-A relationship).
Syntax: class Child extends Parent
3. Access Level Cannot Decrease
Child's method access must be same or more permissive than parent's.
Valid: protected → public | Invalid: public → private
4. Cannot Override Static Methods
Static methods belong to the class, not instances. They're hidden, not overridden.
Note: You can have same static method name in child, but it's method hiding
5. Cannot Override Final Methods
Methods marked as final are locked and cannot be changed by subclasses.
Purpose: final prevents modification of critical method behavior
6. Private Methods Not Inherited
Private methods aren't visible to child classes, so they can't be overridden.
Result: Creating same-named method in child is a new method, not override
The @Override Annotation
Use @Override to let the compiler verify you're actually overriding. It catches typos and signature mismatches at compile time.
Example: Benefits of @Override
class Vehicle {
void startEngine() {
System.out.println("Engine starting...");
}
}
class Car extends Vehicle {
// Correct override
@Override
void startEngine() {
System.out.println("Car engine started with ignition key");
}
// Typo in method name - @Override catches this error!
// @Override
// void startEngien() { // Compile error: method doesn't exist in parent
// System.out.println("Typo version");
// }
// Wrong parameters - @Override catches this too!
// @Override
// void startEngine(String key) { // Compile error: signature mismatch
// System.out.println("With key: " + key);
// }
}
public class AnnotationDemo {
public static void main(String[] args) {
Vehicle myCar = new Car();
myCar.startEngine();
}
}
Car engine started with ignition key
Explanation:
@Overridetells compiler to verify this method overrides a parent method- Prevents accidental creation of new methods due to typos
- Makes code intention clear and improves maintainability
- Optional but considered best practice in professional code
Using super to Call Parent Methods
Sometimes you want to extend parent functionality rather than completely replace it. Use super keyword to call the parent's version.
Example: Extending Parent Behavior
class Employee {
String name;
double salary;
Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
void displayInfo() {
System.out.println("Name: " + name);
System.out.println("Salary: $" + salary);
}
}
class Manager extends Employee {
String department;
Manager(String name, double salary, String department) {
super(name, salary);
this.department = department;
}
@Override
void displayInfo() {
// Call parent's method first
super.displayInfo();
// Add additional information
System.out.println("Department: " + department);
System.out.println("Role: Manager");
}
}
public class SuperExample {
public static void main(String[] args) {
Manager mgr = new Manager("Alice Johnson", 75000, "IT");
mgr.displayInfo();
}
}
Name: Alice Johnson Salary: $75000.0 Department: IT Role: Manager
Explanation:
super.displayInfo()calls the parent's version of the method- Allows code reuse without duplicating parent's logic
- Child method extends functionality instead of replacing entirely
- super is required to explicitly invoke parent's overridden method
Real-World Example: Payment System
See how overriding enables flexible payment processing for different payment methods.
Example: Payment Processing
class Payment {
double amount;
Payment(double amount) {
this.amount = amount;
}
void processPayment() {
System.out.println("Processing payment of $" + amount);
}
void generateReceipt() {
System.out.println("Receipt generated for $" + amount);
}
}
class CreditCardPayment extends Payment {
String cardNumber;
CreditCardPayment(double amount, String cardNumber) {
super(amount);
this.cardNumber = cardNumber;
}
@Override
void processPayment() {
System.out.println("Processing credit card payment...");
System.out.println("Card: **** **** **** " + cardNumber.substring(12));
System.out.println("Amount: $" + amount);
System.out.println("Payment approved ✓");
}
}
class PayPalPayment extends Payment {
String email;
PayPalPayment(double amount, String email) {
super(amount);
this.email = email;
}
@Override
void processPayment() {
System.out.println("Processing PayPal payment...");
System.out.println("Account: " + email);
System.out.println("Amount: $" + amount);
System.out.println("Payment sent via PayPal ✓");
}
}
class CashPayment extends Payment {
CashPayment(double amount) {
super(amount);
}
@Override
void processPayment() {
System.out.println("Processing cash payment...");
System.out.println("Amount received: $" + amount);
System.out.println("Thank you! ✓");
}
}
public class PaymentSystem {
public static void main(String[] args) {
Payment payment1 = new CreditCardPayment(150.00, "1234567890123456");
Payment payment2 = new PayPalPayment(75.50, "user@email.com");
Payment payment3 = new CashPayment(50.00);
payment1.processPayment();
System.out.println();
payment2.processPayment();
System.out.println();
payment3.processPayment();
}
}
Processing credit card payment... Card: **** **** **** 3456 Amount: $150.0 Payment approved ✓ Processing PayPal payment... Account: user@email.com Amount: $75.5 Payment sent via PayPal ✓ Processing cash payment... Amount received: $50.0 Thank you! ✓
Explanation:
- Each payment type overrides processPayment() with its own logic
- System can handle any payment type through parent reference
- Easy to add new payment methods without changing existing code
- Demonstrates real-world polymorphism for flexible system design
Covariant Return Types
Java allows changing return type when overriding, as long as the new type is a subclass of the original. This is called covariant return type.
Example: Covariant Return Type
class Animal {
Animal getBaby() {
System.out.println("Returning an animal baby");
return new Animal();
}
}
class Dog extends Animal {
@Override
Dog getBaby() { // Return type changed from Animal to Dog
System.out.println("Returning a puppy");
return new Dog();
}
}
class Cat extends Animal {
@Override
Cat getBaby() { // Return type changed from Animal to Cat
System.out.println("Returning a kitten");
return new Cat();
}
}
public class CovariantExample {
public static void main(String[] args) {
Animal animal = new Animal();
Animal baby1 = animal.getBaby();
Dog dog = new Dog();
Dog puppy = dog.getBaby(); // Returns Dog, not just Animal
Cat cat = new Cat();
Cat kitten = cat.getBaby(); // Returns Cat, not just Animal
System.out.println("\nAll babies created successfully!");
}
}
Returning an animal baby Returning a puppy Returning a kitten All babies created successfully!
Explanation:
- Parent returns Animal, children return Dog/Cat (subclasses of Animal)
- Valid because Dog and Cat have IS-A relationship with Animal
- Provides more specific return types in subclasses
- Feature introduced in Java 5 for better type safety
Overloading vs Overriding Comparison
Method Overloading
Same method name, different parameters within the same class. Resolved at compile time.
Example: print(int x), print(String s), print(int x, int y)
Method Overriding
Same method signature in parent and child classes. Resolved at runtime based on object type.
Example: Parent and child both have void display()
Key Differences
Overloading = compile-time polymorphism, same class. Overriding = runtime polymorphism, inheritance needed.
Purpose: Overloading for convenience, overriding for specialized behavior
Common Overriding Mistakes
- Parameter mismatch: Different parameters create overloading, not overriding
- Missing @Override: Typos can accidentally create new methods instead
- Reducing access: Cannot make public method protected/private in child
- Trying to override static: Static methods are hidden, not overridden
- Final method override: Cannot override methods marked as final
- Private method confusion: Private methods aren't inherited, so can't be overridden
- Constructor confusion: Constructors cannot be overridden (not inherited)
⚠️ Critical Rule: For overriding, method signature (name + parameters + return type) must match exactly. Even changing one parameter type makes it overloading!
Key Takeaways
- Definition: Child class provides specific implementation of parent's method
- Purpose: Enable runtime polymorphism and specialized behavior
- Requirement: Inheritance (IS-A relationship) is mandatory
- Signature: Must match exactly - name, parameters, return type
- @Override: Always use annotation to catch errors early
- Runtime decision: JVM picks method based on actual object, not reference
- Access levels: Can widen (protected to public) but not narrow
- super keyword: Access parent's overridden method when needed
🎯 Best Practice: Always use @Override annotation. It's your safety net against bugs and makes your code intention crystal clear to other developers!