Introduction
Imagine you have a generic recipe for making coffee, but you want to create a special version with extra ingredients while keeping the same basic process. That's exactly what method overriding does in Java! It allows a subclass to provide a specific implementation of a method that's already defined in its superclass.
Method overriding is like customizing inherited behavior—you keep the method signature but change what the method actually does to fit the subclass's specific needs.
What is Method Overriding?
Method overriding occurs when a subclass provides a specific implementation for a method that is already defined in its parent class. The overridden method in the subclass must have the same signature as the method in the parent class.
Key Characteristics:
- 🔄 Runtime polymorphism: Method resolution happens at runtime
- 🎯 Same signature: Method name, parameters, and return type must match
- 📈 IS-A relationship: Only possible through inheritance
- 🚫 Access cannot be restricted: Can't reduce visibility
- ✅ Covariant return types: Allowed in specific cases
Code Explanation with Examples
Example 1: Basic Method Overriding
public class BasicOverriding { public static void main(String[] args) { System.out.println("=== BASIC METHOD OVERRIDING ==="); // Creating objects Animal genericAnimal = new Animal(); Dog myDog = new Dog(); Cat myCat = new Cat(); // Calling overridden methods System.out.println("Animal sound: " + genericAnimal.makeSound()); System.out.println("Dog sound: " + myDog.makeSound()); System.out.println("Cat sound: " + myCat.makeSound()); // Polymorphism in action System.out.println("\n=== POLYMORPHISM DEMONSTRATION ==="); Animal animal1 = new Dog(); // Animal reference, Dog object Animal animal2 = new Cat(); // Animal reference, Cat object System.out.println("Animal1 sound: " + animal1.makeSound()); // Woof! Woof! System.out.println("Animal2 sound: " + animal2.makeSound()); // Meow! Meow! // Method with parameters overriding System.out.println("\n=== METHOD WITH PARAMETERS ==="); System.out.println("Animal move: " + genericAnimal.move(5)); System.out.println("Dog move: " + myDog.move(5)); System.out.println("Cat move: " + myCat.move(5)); } } class Animal { // Method to be overridden public String makeSound() { return "Some generic animal sound"; } public String move(int distance) { return "Animal moves " + distance + " meters"; } // Method that won't be overridden public void breathe() { System.out.println("Animal is breathing..."); } } class Dog extends Animal { // Overriding makeSound method @Override public String makeSound() { return "Woof! Woof!"; } // Overriding move method with specific behavior @Override public String move(int distance) { return "Dog runs " + distance + " meters happily"; } // Additional method specific to Dog public void fetch() { System.out.println("Dog is fetching the ball!"); } } class Cat extends Animal { // Overriding makeSound method @Override public String makeSound() { return "Meow! Meow!"; } // Overriding move method with specific behavior @Override public String move(int distance) { return "Cat gracefully moves " + distance + " meters"; } // Additional method specific to Cat public void climb() { System.out.println("Cat is climbing the tree!"); } } Output:
=== BASIC METHOD OVERRIDING === Animal sound: Some generic animal sound Dog sound: Woof! Woof! Cat sound: Meow! Meow! === POLYMORPHISM DEMONSTRATION === Animal1 sound: Woof! Woof! Animal2 sound: Meow! Meow! === METHOD WITH PARAMETERS === Animal move: Animal moves 5 meters Dog move: Dog runs 5 meters happily Cat move: Cat gracefully moves 5 meters
Example 2: Overriding Rules and @Override Annotation
public class OverridingRules { public static void main(String[] args) { System.out.println("=== METHOD OVERRIDING RULES ==="); // Testing different scenarios Parent parent = new Parent(); Child child = new Child(); System.out.println("Parent method: " + parent.doSomething()); System.out.println("Child method: " + child.doSomething()); // Access modifier rules System.out.println("\n=== ACCESS MODIFIER RULES ==="); System.out.println("Parent protected: " + parent.protectedMethod()); System.out.println("Child protected: " + child.protectedMethod()); // Return type rules System.out.println("\n=== RETURN TYPE RULES ==="); Parent pRef = new Child(); System.out.println("Covariant return: " + pRef.getObject()); // Exception handling rules System.out.println("\n=== EXCEPTION HANDLING RULES ==="); try { child.methodWithException(); } catch (Exception e) { System.out.println("Caught exception: " + e.getMessage()); } // Final method demonstration System.out.println("\n=== FINAL METHODS ==="); System.out.println("Parent final: " + parent.finalMethod()); System.out.println("Child final: " + child.finalMethod()); // Inherited, not overridden } } class Parent { // Regular method - can be overridden public String doSomething() { return "Parent doing something"; } // Protected method - can be overridden with same or broader access protected String protectedMethod() { return "Parent protected method"; } // Method with return type - can use covariant return type public Object getObject() { return new Object(); } // Method that throws exception public void methodWithException() throws Exception { throw new Exception("Exception from Parent"); } // Final method - cannot be overridden public final String finalMethod() { return "This is final in Parent"; } // Private method - not inherited, so cannot be overridden private String privateMethod() { return "Private method in Parent"; } // Static method - not overridden, but can be hidden public static String staticMethod() { return "Static method in Parent"; } } class Child extends Parent { // ✅ CORRECT: Same signature, public access (same as parent) @Override public String doSomething() { return "Child doing something differently"; } // ✅ CORRECT: Can change protected to public (broader access) @Override public String protectedMethod() { return "Child protected method - now public"; } // ✅ CORRECT: Covariant return type (String is subclass of Object) @Override public String getObject() { return "Child returning String instead of Object"; } // ✅ CORRECT: Can throw same exception or subclass exception @Override public void methodWithException() throws RuntimeException { throw new RuntimeException("RuntimeException from Child"); } // ❌ WRONG: Cannot reduce visibility (commented out to avoid error) /* @Override private String doSomething() { // COMPILER ERROR: cannot reduce visibility return "This won't compile"; } */ // ❌ WRONG: Cannot override final method (commented out to avoid error) /* @Override public final String finalMethod() { // COMPILER ERROR: cannot override final return "This won't compile"; } */ // This is NOT overriding - it's method hiding (different from overriding) public static String staticMethod() { return "Static method in Child - this is method hiding, not overriding"; } // This is NOT overriding - private methods are not inherited private String privateMethod() { return "This is a new method in Child, not overriding"; } } // Demonstration of abstract method overriding abstract class Shape { // Abstract method - must be overridden by concrete subclasses public abstract double calculateArea(); // Concrete method - can be overridden public String getDescription() { return "This is a shape"; } } class Circle extends Shape { private double radius; public Circle(double radius) { this.radius = radius; } // ✅ MUST override abstract method @Override public double calculateArea() { return Math.PI * radius * radius; } // ✅ CAN override concrete method @Override public String getDescription() { return "This is a circle with radius " + radius; } } class Rectangle extends Shape { private double width; private double height; public Rectangle(double width, double height) { this.width = width; this.height = height; } // ✅ MUST override abstract method @Override public double calculateArea() { return width * height; } // Choosing NOT to override getDescription() - uses parent's version } Output:
=== METHOD OVERRIDING RULES === Parent method: Parent doing something Child method: Child doing something differently === ACCESS MODIFIER RULES === Parent protected: Parent protected method Child protected: Child protected method - now public === RETURN TYPE RULES === Covariant return: Child returning String instead of Object === EXCEPTION HANDLING RULES === Caught exception: RuntimeException from Child === FINAL METHODS === Parent final: This is final in Parent Child final: This is final in Parent
Example 3: Real-World Overriding Examples
public class RealWorldOverriding { public static void main(String[] args) { System.out.println("=== REAL-WORLD OVERRIDING EXAMPLES ==="); // Payment processing system System.out.println("\n=== PAYMENT PROCESSING ==="); PaymentProcessor creditCard = new CreditCardProcessor(); PaymentProcessor paypal = new PayPalProcessor(); PaymentProcessor crypto = new CryptoProcessor(); processPayment(creditCard, 100.0); processPayment(paypal, 150.0); processPayment(crypto, 200.0); // Employee management system System.out.println("\n=== EMPLOYEE MANAGEMENT ==="); Employee manager = new Manager("Alice", 75000, 10000); Employee developer = new Developer("Bob", 60000, 5); Employee intern = new Intern("Charlie", 30000); displayEmployeeInfo(manager); displayEmployeeInfo(developer); displayEmployeeInfo(intern); // Notification system System.out.println("\n=== NOTIFICATION SYSTEM ==="); Notification email = new EmailNotification(); Notification sms = new SMSNotification(); Notification push = new PushNotification(); sendNotification(email, "Hello via Email!"); sendNotification(sms, "Hello via SMS!"); sendNotification(push, "Hello via Push!"); } // Polymorphic method - works with any PaymentProcessor public static void processPayment(PaymentProcessor processor, double amount) { System.out.println("Processing $" + amount + " payment..."); processor.processPayment(amount); System.out.println("Fee: $" + processor.calculateFee(amount)); System.out.println(); } // Polymorphic method - works with any Employee public static void displayEmployeeInfo(Employee employee) { System.out.println("Name: " + employee.getName()); System.out.println("Base Salary: $" + employee.getBaseSalary()); System.out.println("Total Salary: $" + employee.calculateSalary()); System.out.println("Role: " + employee.getRole()); System.out.println(); } // Polymorphic method - works with any Notification public static void sendNotification(Notification notification, String message) { notification.send(message); System.out.println(); } } // Payment Processing System abstract class PaymentProcessor { protected String processorName; public PaymentProcessor(String name) { this.processorName = name; } // Abstract method - must be overridden public abstract void processPayment(double amount); // Concrete method - can be overridden public double calculateFee(double amount) { return amount * 0.02; // 2% default fee } // Final method - cannot be overridden public final String getProcessorName() { return processorName; } } class CreditCardProcessor extends PaymentProcessor { public CreditCardProcessor() { super("Credit Card"); } @Override public void processPayment(double amount) { System.out.println("Processing credit card payment of $" + amount); System.out.println("Validating card details..."); System.out.println("Authorizing transaction..."); System.out.println("Payment completed successfully!"); } @Override public double calculateFee(double amount) { return amount * 0.025; // 2.5% fee for credit cards } } class PayPalProcessor extends PaymentProcessor { public PayPalProcessor() { super("PayPal"); } @Override public void processPayment(double amount) { System.out.println("Processing PayPal payment of $" + amount); System.out.println("Redirecting to PayPal..."); System.out.println("Confirming payment..."); System.out.println("Payment completed successfully!"); } @Override public double calculateFee(double amount) { return amount * 0.029 + 0.30; // 2.9% + $0.30 fixed fee } } class CryptoProcessor extends PaymentProcessor { public CryptoProcessor() { super("Cryptocurrency"); } @Override public void processPayment(double amount) { System.out.println("Processing cryptocurrency payment of $" + amount); System.out.println("Converting to BTC..."); System.out.println("Confirming blockchain transaction..."); System.out.println("Payment completed successfully!"); } @Override public double calculateFee(double amount) { return amount * 0.01; // 1% fee for crypto (lower fees) } } // Employee Management System class Employee { protected String name; protected double baseSalary; public Employee(String name, double baseSalary) { this.name = name; this.baseSalary = baseSalary; } public String getName() { return name; } public double getBaseSalary() { return baseSalary; } // Method to be overridden by subclasses public double calculateSalary() { return baseSalary; } // Method to be overridden by subclasses public String getRole() { return "Employee"; } } class Manager extends Employee { private double bonus; public Manager(String name, double baseSalary, double bonus) { super(name, baseSalary); this.bonus = bonus; } @Override public double calculateSalary() { return baseSalary + bonus; // Salary includes bonus } @Override public String getRole() { return "Manager"; } // Additional method specific to Manager public void conductMeeting() { System.out.println(name + " is conducting a team meeting"); } } class Developer extends Employee { private int yearsOfExperience; public Developer(String name, double baseSalary, int yearsOfExperience) { super(name, baseSalary); this.yearsOfExperience = yearsOfExperience; } @Override public double calculateSalary() { // Developers get experience-based bonus double experienceBonus = baseSalary * (yearsOfExperience * 0.05); return baseSalary + experienceBonus; } @Override public String getRole() { return "Developer with " + yearsOfExperience + " years experience"; } // Additional method specific to Developer public void writeCode() { System.out.println(name + " is writing code"); } } class Intern extends Employee { public Intern(String name, double baseSalary) { super(name, baseSalary); } @Override public String getRole() { return "Intern"; } // Interns don't override calculateSalary() - they use the base version } // Notification System interface Notification { void send(String message); String getType(); } class EmailNotification implements Notification { @Override public void send(String message) { System.out.println("Sending Email Notification:"); System.out.println("From: [email protected]"); System.out.println("To: [email protected]"); System.out.println("Subject: Notification"); System.out.println("Body: " + message); System.out.println("Email sent successfully!"); } @Override public String getType() { return "Email"; } } class SMSNotification implements Notification { @Override public void send(String message) { System.out.println("Sending SMS Notification:"); System.out.println("To: +1234567890"); System.out.println("Message: " + message); System.out.println("SMS sent successfully!"); } @Override public String getType() { return "SMS"; } } class PushNotification implements Notification { @Override public void send(String message) { System.out.println("Sending Push Notification:"); System.out.println("Device: User's Smartphone"); System.out.println("Title: App Notification"); System.out.println("Body: " + message); System.out.println("Push notification sent successfully!"); } @Override public String getType() { return "Push"; } } Output:
=== REAL-WORLD OVERRIDING EXAMPLES === === PAYMENT PROCESSING === Processing $100.0 payment... Processing credit card payment of $100.0 Validating card details... Authorizing transaction... Payment completed successfully! Fee: $2.5 Processing $150.0 payment... Processing PayPal payment of $150.0 Redirecting to PayPal... Confirming payment... Payment completed successfully! Fee: $4.65 Processing $200.0 payment... Processing cryptocurrency payment of $200.0 Converting to BTC... Confirming blockchain transaction... Payment completed successfully! Fee: $2.0 === EMPLOYEE MANAGEMENT === Name: Alice Base Salary: $75000.0 Total Salary: $85000.0 Role: Manager Name: Bob Base Salary: $60000.0 Total Salary: $75000.0 Role: Developer with 5 years experience Name: Charlie Base Salary: $30000.0 Total Salary: $30000.0 Role: Intern === NOTIFICATION SYSTEM === Sending Email Notification: From: [email protected] To: [email protected] Subject: Notification Body: Hello via Email! Email sent successfully! Sending SMS Notification: To: +1234567890 Message: Hello via SMS! SMS sent successfully! Sending Push Notification: Device: User's Smartphone Title: App Notification Body: Hello via Push! Push notification sent successfully!
Example 4: Advanced Overriding Concepts
public class AdvancedOverriding { public static void main(String[] args) { System.out.println("=== ADVANCED OVERRIDING CONCEPTS ==="); // Covariant return types System.out.println("=== COVARIANT RETURN TYPES ==="); AnimalFactory animalFactory = new DogFactory(); Animal animal = animalFactory.createAnimal(); System.out.println("Created: " + animal.getClass().getSimpleName()); // Exception handling in overriding System.out.println("\n=== EXCEPTION HANDLING ==="); DataReader fileReader = new FileDataReader(); DataReader networkReader = new NetworkDataReader(); try { fileReader.readData(); } catch (DataReadException e) { System.out.println("File read error: " + e.getMessage()); } try { networkReader.readData(); } catch (DataReadException e) { System.out.println("Network read error: " + e.getMessage()); } // Using super in overriding System.out.println("\n=== USING SUPER IN OVERRIDING ==="); AdvancedChild child = new AdvancedChild(); child.display(); // Method hiding vs overriding System.out.println("\n=== METHOD HIDING VS OVERRIDING ==="); ParentClass parentRef = new ChildClass(); ChildClass childRef = new ChildClass(); // Instance method - overriding (runtime polymorphism) parentRef.instanceMethod(); // Child's version (overriding) childRef.instanceMethod(); // Child's version // Static method - hiding (compile-time polymorphism) parentRef.staticMethod(); // Parent's version (hiding) childRef.staticMethod(); // Child's version ParentClass.staticMethod(); // Parent's version ChildClass.staticMethod(); // Child's version } } // Covariant return type example class Animal { public String getName() { return "Generic Animal"; } } class Dog extends Animal { @Override public String getName() { return "Dog"; } public void bark() { System.out.println("Woof! Woof!"); } } class AnimalFactory { public Animal createAnimal() { return new Animal(); } } class DogFactory extends AnimalFactory { // ✅ Covariant return type - returning Dog instead of Animal @Override public Dog createAnimal() { return new Dog(); } } // Exception handling in overriding class DataReadException extends Exception { public DataReadException(String message) { super(message); } } class DataReader { public void readData() throws DataReadException { throw new DataReadException("Generic data read error"); } // Method that throws multiple exceptions public void processData() throws IOException, SQLException { // Some processing } } class FileDataReader extends DataReader { @Override public void readData() throws DataReadException { throw new DataReadException("File not found or corrupted"); } // ✅ Can throw same exceptions @Override public void processData() throws IOException, SQLException { // File processing } } class NetworkDataReader extends DataReader { @Override public void readData() throws DataReadException { throw new DataReadException("Network connection failed"); } // ✅ Can throw subclass exceptions @Override public void processData() throws FileNotFoundException, SQLException { // Network processing - FileNotFoundException is subclass of IOException } // ❌ Cannot throw broader exceptions (commented out to avoid error) /* @Override public void processData() throws Exception { // Exception is broader than IOException // This won't compile } */ } // Using super in overriding class AdvancedParent { protected String value = "Parent Value"; public void display() { System.out.println("Parent display: " + value); } public void showMessage() { System.out.println("Message from Parent"); } } class AdvancedChild extends AdvancedParent { private String value = "Child Value"; @Override public void display() { // Call parent's method using super super.display(); // Then add child-specific behavior System.out.println("Child display: " + this.value); System.out.println("Parent's value via super: " + super.value); } @Override public void showMessage() { // Can choose when to call parent's method System.out.println("Before calling parent's method"); super.showMessage(); System.out.println("After calling parent's method"); } public void demonstrateSuper() { // Using super to access parent's method super.showMessage(); // Using this to access current class's method this.showMessage(); } } // Method hiding vs overriding class ParentClass { // Instance method - can be overridden public void instanceMethod() { System.out.println("ParentClass instanceMethod"); } // Static method - can be hidden, not overridden public static void staticMethod() { System.out.println("ParentClass staticMethod"); } } class ChildClass extends ParentClass { // ✅ Overriding instance method @Override public void instanceMethod() { System.out.println("ChildClass instanceMethod - OVERRIDDEN"); } // ❌ This is method hiding, not overriding public static void staticMethod() { System.out.println("ChildClass staticMethod - HIDDEN"); } // ❌ Cannot use @Override with static method (commented out to avoid error) /* @Override public static void staticMethod() { // COMPILER ERROR System.out.println("This won't compile"); } */ } // Interface method overriding interface Vehicle { default void start() { System.out.println("Vehicle starting..."); } void move(); } class Car implements Vehicle { // ✅ Can override default method @Override public void start() { System.out.println("Car starting with key..."); } // ✅ Must implement abstract method @Override public void move() { System.out.println("Car moving on road..."); } } class ElectricCar extends Car { // ✅ Can override again @Override public void start() { System.out.println("Electric car starting silently..."); } } Output:
=== ADVANCED OVERRIDING CONCEPTS === === COVARIANT RETURN TYPES === Created: Dog === EXCEPTION HANDLING === File read error: File not found or corrupted Network read error: Network connection failed === USING SUPER IN OVERRIDING === Parent display: Parent Value Child display: Child Value Parent's value via super: Parent Value === METHOD HIDING VS OVERRIDING === ChildClass instanceMethod - OVERRIDDEN ChildClass instanceMethod - OVERRIDDEN ParentClass staticMethod ChildClass staticMethod - HIDDEN ParentClass staticMethod ChildClass staticMethod - HIDDEN
Example 5: Common Pitfalls and Best Practices
public class OverridingPitfalls { public static void main(String[] args) { System.out.println("=== COMMON PITFALLS AND BEST PRACTICES ==="); // Pitfall 1: Accidental overloading instead of overriding System.out.println("=== PITFALL 1: ACCIDENTAL OVERLOADING ==="); ProblematicChild problemChild = new ProblematicChild(); problemChild.doSomething("test"); // Calls child's method (overloading) // To call parent's method, we need to cast ProblematicParent problemParent = problemChild; problemParent.doSomething(); // Calls parent's method // Pitfall 2: Forgetting @Override annotation System.out.println("\n=== PITFALL 2: MISSING @OVERRIDE ==="); MissingOverrideChild missingOverride = new MissingOverrideChild(); missingOverride.doSomething(); // Intended to override but doesn't! // Best practices demonstration System.out.println("\n=== BEST PRACTICES ==="); GoodChild goodChild = new GoodChild(); goodChild.properlyOverriddenMethod(); goodChild.useSuperAppropriately(); // Demonstration of proper equals and hashCode overriding System.out.println("\n=== EQUALS AND HASHCODE ==="); Person person1 = new Person("Alice", 25); Person person2 = new Person("Alice", 25); Person person3 = new Person("Bob", 30); System.out.println("person1.equals(person2): " + person1.equals(person2)); System.out.println("person1.equals(person3): " + person1.equals(person3)); System.out.println("person1 hashCode: " + person1.hashCode()); System.out.println("person2 hashCode: " + person2.hashCode()); System.out.println("person3 hashCode: " + person3.hashCode()); // toString overriding demonstration System.out.println("\n=== TOSTRING OVERRIDING ==="); System.out.println("person1: " + person1); System.out.println("person2: " + person2); System.out.println("person3: " + person3); } } // Pitfall 1: Accidental overloading class ProblematicParent { public void doSomething() { System.out.println("Parent doSomething() - no parameters"); } } class ProblematicChild extends ProblematicParent { // ❌ PITFALL: This is overloading, not overriding! // Different signature (added parameter) public void doSomething(String parameter) { System.out.println("Child doSomething(String) - this is OVERLOADING, not overriding!"); } // ✅ The correct overriding would be: // @Override // public void doSomething() { // System.out.println("Child doSomething() - properly overridden"); // } } // Pitfall 2: Forgetting @Override annotation class MissingOverrideParent { public void doSomething() { System.out.println("Parent doSomething()"); } } class MissingOverrideChild extends MissingOverrideParent { // ❌ PITFALL: Misspelled method name, but no compiler error without @Override public void doSomethin() { // Missing 'g' - should be doSomething System.out.println("Child doSomethin() - this is a NEW method, not overriding!"); } // ✅ With @Override, we'd get a compiler error for the misspelling: /* @Override public void doSomethin() { // COMPILER ERROR: method does not override System.out.println("This won't compile with @Override annotation"); } */ } // Best practices example class GoodParent { protected String data = "Parent Data"; public void properlyOverriddenMethod() { System.out.println("GoodParent: properlyOverriddenMethod"); } public void useSuperAppropriately() { System.out.println("GoodParent: useSuperAppropriately"); } // Method designed for extension protected void templateMethod() { System.out.println("GoodParent: Starting template method"); doCustomPart(); System.out.println("GoodParent: Ending template method"); } // Hook method - meant to be overridden protected void doCustomPart() { System.out.println("GoodParent: Default custom part"); } } class GoodChild extends GoodParent { // ✅ BEST PRACTICE: Always use @Override annotation @Override public void properlyOverriddenMethod() { System.out.println("GoodChild: properlyOverriddenMethod - correctly overridden with @Override"); } @Override public void useSuperAppropriately() { // ✅ BEST PRACTICE: Call super when it makes sense super.useSuperAppropriately(); System.out.println("GoodChild: useSuperAppropriately - extending parent behavior"); } @Override protected void doCustomPart() { // ✅ BEST PRACTICE: Override hook methods to customize behavior System.out.println("GoodChild: Custom implementation of doCustomPart"); } public void demonstrateTemplate() { System.out.println("Demonstrating template method pattern:"); templateMethod(); } } // Proper equals and hashCode overriding class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } // ✅ PROPER equals() OVERRIDING @Override public boolean equals(Object obj) { // 1. Check if same object reference if (this == obj) return true; // 2. Check if null or different class if (obj == null || this.getClass() != obj.getClass()) return false; // 3. Cast to Person Person other = (Person) obj; // 4. Compare significant fields return age == other.age && (name == null ? other.name == null : name.equals(other.name)); } // ✅ PROPER hashCode() OVERRIDING @Override public int hashCode() { int result = 17; result = 31 * result + (name != null ? name.hashCode() : 0); result = 31 * result + age; return result; } // ✅ PROPER toString() OVERRIDING @Override public String toString() { return String.format("Person{name='%s', age=%d}", name, age); } // ❌ COMMON MISTAKE: Overloading equals instead of overriding public boolean equals(Person other) { // This is OVERLOADING, not overriding! if (other == null) return false; return age == other.age && (name == null ? other.name == null : name.equals(other.name)); } } // Common pitfalls with access modifiers class AccessModifierParent { public void publicMethod() { System.out.println("Parent public method"); } protected void protectedMethod() { System.out.println("Parent protected method"); } } class AccessModifierChild extends AccessModifierParent { // ✅ Can keep public access @Override public void publicMethod() { System.out.println("Child public method - same access"); } // ✅ Can increase access (protected to public) @Override public void protectedMethod() { System.out.println("Child protected method - now public"); } // ❌ Cannot decrease access (commented out to avoid error) /* @Override protected void publicMethod() { // COMPILER ERROR: cannot reduce visibility System.out.println("This won't compile"); } */ } // Final class and methods final class FinalClass { public void normalMethod() { System.out.println("Normal method in final class"); } public final void finalMethod() { System.out.println("Final method - cannot be overridden"); } } // ❌ Cannot extend final class (commented out to avoid error) /* class FinalClassChild extends FinalClass { // COMPILER ERROR: cannot inherit from final @Override public void finalMethod() { // This also won't compile System.out.println("Cannot override final method"); } } */ Output:
=== COMMON PITFALLS AND BEST PRACTICES === === PITFALL 1: ACCIDENTAL OVERLOADING === Child doSomething(String) - this is OVERLOADING, not overriding! Parent doSomething() - no parameters === PITFALL 2: MISSING @OVERRIDE === Child doSomethin() - this is a NEW method, not overriding! === BEST PRACTICES === GoodChild: properlyOverriddenMethod - correctly overridden with @Override GoodParent: useSuperAppropriately GoodChild: useSuperAppropriately - extending parent behavior Demonstrating template method pattern: GoodParent: Starting template method GoodChild: Custom implementation of doCustomPart GoodParent: Ending template method === EQUALS AND HASHCODE === person1.equals(person2): true person1.equals(person3): false person1 hashCode: 92959423 person2 hashCode: 92959423 person3 hashCode: 83460091 === TOSTRING OVERRIDING === person1: Person{name='Alice', age=25} person2: Person{name='Alice', age=25} person3: Person{name='Bob', age=30} Method Overriding Rules Summary
| Rule | Description | Example |
|---|---|---|
| Same Signature | Method name, parameters, and return type must match | void method(int x) → void method(int x) |
| IS-A Relationship | Only through inheritance | Child extends Parent |
| Access Modifier | Can't reduce visibility | public → public (not private) |
| Final Methods | Cannot override final methods | public final void method() |
| Static Methods | Cannot override, but can hide | Method hiding, not overriding |
| Covariant Returns | Can return subclass of return type | Animal → Dog |
| Exception Handling | Can throw same or subclass exceptions | IOException → FileNotFoundException |
@Override Annotation Benefits
- Compiler checking: Ensures method actually overrides
- Readability: Clearly indicates overriding intent
- Maintenance: Prevents accidental overloading
- Documentation: Shows design decisions
Best Practices
✅ Do:
- Always use @Override annotation
- Call super.method() when extending behavior
- Follow Liskov Substitution Principle
- Override toString(), equals(), hashCode() properly
- Use covariant return types when appropriate
- Document behavior changes in overridden methods
❌ Don't:
- Change method semantics when overriding
- Forget to call super when it's needed
- Override with completely different behavior
- Ignore exception specifications
- Override static methods (they're hidden, not overridden)
Common Pitfalls
- Accidental overloading instead of overriding
- Forgetting @Override annotation
- Changing method semantics unexpectedly
- Not calling super when parent behavior is needed
- Reducing access visibility
- Overriding with incompatible exceptions
Performance Considerations
- Virtual method calls have slight overhead
- JVM optimizations like inline caching help
- Final methods can be inlined by JIT compiler
- Interface method calls are optimized in modern JVMs
Conclusion
Method overriding is Java's mechanism for runtime polymorphism and behavior customization:
- 🔄 Runtime binding: Method resolution at execution time
- 🎯 Same signature: Maintains contract with superclass
- 📈 IS-A relationship: Fundamental to inheritance
- 🚫 Controlled changes: Rules prevent breaking changes
- ✅ @Override safety: Compiler checks for correct overriding
Key Takeaways:
- Use @Override always for compiler safety
- Maintain method contracts when overriding
- Call super.method() to preserve parent behavior
- Follow access rules - don't reduce visibility
- Handle exceptions properly - same or subclasses
Mastering method overriding is essential for creating flexible, maintainable object-oriented designs that leverage polymorphism effectively!