🚀 Method Overriding in Java: A Comprehensive Guide
🌟 Introduction to Method Overriding in Java
Method overriding is a fundamental concept in object-oriented programming that allows a subclass to provide a specific implementation of a method that is already defined in its parent class. This powerful feature is one of the cornerstones of polymorphism in Java, enabling objects to behave differently based on their actual class, even when they're referenced by a common superclass type.
When a subclass overrides a method from its superclass, it essentially says, "I know my parent has a way of doing this, but I need to do it differently." This capability allows for more specialized behavior in derived classes while maintaining a common interface defined by the parent class.
In this comprehensive tutorial, we'll explore:
- What method overriding is and how it differs from method overloading
- The rules and conditions for properly overriding methods
- How access modifiers, return types, and exceptions affect overriding
- Common pitfalls and best practices
- Real-world applications and use cases
Whether you're just starting with Java or looking to deepen your understanding of object-oriented principles, this guide will provide you with a solid foundation in method overriding.
🧩 Understanding Java Method Overriding
What is Method Overriding in Java?
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 name as the method in the parent class
- The same parameter list (number, type, and order of parameters)
- A compatible return type (more on this later)
Here's a simple example:
// Parent class
class Animal {
public void makeSound() {
System.out.println("The animal makes a sound");
}
}
// Child class
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("The dog barks");
}
}
In this example, the Dog
class overrides the makeSound()
method from the Animal
class to provide a more specific implementation.
Java Method Overriding vs. Method Overloading
It's important to distinguish between method overriding and method overloading, as they are often confused:
Feature | Method Overriding | Method Overloading |
---|---|---|
Definition | Providing a new implementation for an inherited method | Creating multiple methods with the same name but different parameters |
Inheritance | Requires inheritance relationship | Can occur within the same class |
Method signature | Must have the same name and parameters | Must have the same name but different parameters |
Return type | Must be compatible (covariant) | Can be different |
Runtime/Compile time | Runtime polymorphism | Compile-time polymorphism |
The @Override
Annotation
While not strictly required, it's a best practice to use the @Override
annotation when overriding a method:
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("The dog barks");
}
}
This annotation serves two important purposes:
- It clearly communicates your intention to override a method
- It helps the compiler catch errors if you're not actually overriding a method (e.g., if you misspelled the method name or used different parameters)
📋 Rules and Conditions for Method Overriding in Java
1. Access Modifiers
When overriding a method, the access modifier in the subclass cannot be more restrictive than the access modifier in the superclass. The hierarchy of access modifiers from least restrictive to most restrictive is:
public
protected
- Default (no modifier)
private
Here's what's allowed and what's not:
Superclass Method | Allowed Subclass Method Modifiers |
---|---|
public |
public only |
protected |
public , protected |
Default | public , protected , default |
private |
Cannot be overridden |
Example:
class Parent {
public void methodA() { }
protected void methodB() { }
void methodC() { }
private void methodD() { }
}
class Child extends Parent {
// Valid: same access level
@Override
public void methodA() { }
// Valid: less restrictive access
@Override
public void methodB() { }
// Valid: less restrictive access
@Override
protected void methodC() { }
// Invalid: methodD is private in Parent and cannot be overridden
// This would be a new method, not an override
private void methodD() { }
}
2. Return Types
In Java 5 and later, an overriding method can return a subtype of the return type declared in the superclass method. This is known as covariant return types.
class Animal {
public Animal getAnimal() {
return new Animal();
}
}
class Dog extends Animal {
@Override
public Dog getAnimal() { // Covariant return type
return new Dog();
}
}
In this example, Dog.getAnimal()
returns a Dog
object, which is a subtype of Animal
(the return type of Animal.getAnimal()
).
However, this only works for reference types. For primitive return types, the overriding method must have exactly the same return type.
3. Exception Handling
When it comes to exceptions, an overriding method:
- Can throw any unchecked exceptions (RuntimeException and its subclasses, Error and its subclasses), regardless of whether the superclass method declares them
- Cannot throw checked exceptions that are broader or new compared to those declared by the superclass method
- Can throw narrower checked exceptions or fewer exceptions than the superclass method
Here's a diagram showing the exception hierarchy:
Throwable
├── Error (unchecked)
└── Exception
├── RuntimeException (unchecked)
└── Other Exceptions (checked)
Examples:
import java.io.IOException;
import java.io.FileNotFoundException;
class Parent {
// Declares a checked exception
public void methodA() throws IOException {
// Implementation
}
// Declares no exceptions
public void methodB() {
// Implementation
}
// Declares multiple checked exceptions
public void methodC() throws IOException, ClassNotFoundException {
// Implementation
}
}
class Child extends Parent {
// Valid: throws the same exception
@Override
public void methodA() throws IOException {
// Implementation
}
// Valid: throws a narrower exception
@Override
public void methodA() throws FileNotFoundException { // FileNotFoundException is a subclass of IOException
// Implementation
}
// Valid: throws no exceptions
@Override
public void methodA() {
// Implementation
}
// Invalid: throws a broader exception
@Override
public void methodA() throws Exception { // Exception is broader than IOException
// Implementation
}
// Invalid: throws a new checked exception
@Override
public void methodB() throws IOException { // Parent's methodB doesn't declare IOException
// Implementation
}
// Valid: throws unchecked exceptions
@Override
public void methodB() throws RuntimeException {
// Implementation
}
// Valid: throws fewer exceptions
@Override
public void methodC() throws IOException {
// Implementation
}
}
4. Method Visibility
A method cannot be overridden if it is:
final
: Final methods cannot be overriddenstatic
: Static methods cannot be overridden (they can be hidden, which is different)private
: Private methods are not visible to subclasses, so they cannot be overridden
Example:
class Parent {
public final void finalMethod() {
System.out.println("This is a final method");
}
public static void staticMethod() {
System.out.println("This is a static method");
}
private void privateMethod() {
System.out.println("This is a private method");
}
}
class Child extends Parent {
// Invalid: cannot override a final method
@Override
public void finalMethod() {
// This will cause a compilation error
}
// This is method hiding, not overriding
public static void staticMethod() {
System.out.println("This is a static method in Child");
}
// This is a new method, not an override
private void privateMethod() {
System.out.println("This is a private method in Child");
}
}
5. Abstract Methods
Abstract methods in an abstract class must be overridden in concrete subclasses. They provide a contract that subclasses must fulfill.
abstract class Shape {
// Abstract method - must be overridden by concrete subclasses
public abstract double calculateArea();
}
class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
6. Interface Methods
Methods declared in an interface are implicitly public
and abstract
(unless they are default or static methods in Java 8+). When a class implements an interface, it must override all the abstract methods.
interface Drawable {
void draw(); // Implicitly public and abstract
}
class Rectangle implements Drawable {
@Override
public void draw() {
System.out.println("Drawing a rectangle");
}
}
🔄 Complete Example: Method Overriding in Action
Let's explore a comprehensive example that demonstrates various aspects of method overriding:
Too see full example, Click to expand
import java.io.IOException;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
// Base class for all vehicles
abstract class Vehicle {
private String make;
private String model;
private int year;
public Vehicle(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
}
// Abstract method that must be implemented by all subclasses
public abstract void start() throws IOException;
// Regular method that can be overridden
public void stop() {
System.out.println("Vehicle stopped");
}
// Final method that cannot be overridden
public final String getInfo() {
return year + " " + make + " " + model;
}
// Method with a return type
public Vehicle getVehicle() {
return this;
}
// Method that throws exceptions
public void checkMaintenance() throws IOException, ClassNotFoundException {
System.out.println("Checking vehicle maintenance...");
}
// Protected method
protected void performMaintenance() {
System.out.println("Performing standard maintenance");
}
// Getters
public String getMake() { return make; }
public String getModel() { return model; }
public int getYear() { return year; }
}
// Car class extending Vehicle
class Car extends Vehicle {
private int numDoors;
public Car(String make, String model, int year, int numDoors) {
super(make, model, year);
this.numDoors = numDoors;
}
// Implementing the abstract method
@Override
public void start() {
System.out.println("Car started: Turn key in ignition");
}
// Overriding a regular method
@Override
public void stop() {
System.out.println("Car stopped: Apply brakes and shift to park");
}
// Overriding with covariant return type
@Override
public Car getVehicle() {
return this;
}
// Overriding with narrower exception
@Override
public void checkMaintenance() throws FileNotFoundException {
System.out.println("Checking car maintenance...");
}
// Overriding with less restrictive access modifier
@Override
public void performMaintenance() {
System.out.println("Performing car-specific maintenance");
}
// Car-specific method
public void honk() {
System.out.println("Honk! Honk!");
}
public int getNumDoors() { return numDoors; }
}
// ElectricCar class extending Car
class ElectricCar extends Car {
private int batteryCapacity;
public ElectricCar(String make, String model, int year, int numDoors, int batteryCapacity) {
super(make, model, year, numDoors);
this.batteryCapacity = batteryCapacity;
}
// Overriding the start method with no exceptions
@Override
public void start() {
System.out.println("Electric car started: Press start button");
}
// Overriding with no exceptions
@Override
public void checkMaintenance() {
System.out.println("Checking electric car maintenance...");
}
// Electric car specific method
public void charge() {
System.out.println("Charging electric car...");
}
public int getBatteryCapacity() { return batteryCapacity; }
}
// Motorcycle class extending Vehicle
class Motorcycle extends Vehicle {
private boolean hasSidecar;
public Motorcycle(String make, String model, int year, boolean hasSidecar) {
super(make, model, year);
this.hasSidecar = hasSidecar;
}
// Implementing the abstract method with the same exception
@Override
public void start() throws IOException {
System.out.println("Motorcycle started: Kick start or press button");
if (!hasSidecar) {
System.out.println("Balance required!");
}
}
// Motorcycle-specific method
public void wheelie() {
System.out.println("Performing a wheelie!");
}
public boolean hasSidecar() { return hasSidecar; }
}
// Main class to demonstrate method overriding
public class VehicleDemo {
public static void main(String[] args) {
// Create a list of vehicles
List<Vehicle> vehicles = new ArrayList<>();
vehicles.add(new Car("Toyota", "Camry", 2020, 4));
vehicles.add(new ElectricCar("Tesla", "Model S", 2021, 4, 100));
vehicles.add(new Motorcycle("Harley-Davidson", "Street 750", 2019, false));
// Demonstrate polymorphism through method overriding
for (Vehicle vehicle : vehicles) {
System.out.println("\nVehicle Info: " + vehicle.getInfo());
try {
// This will call the appropriate overridden method based on the actual object type
vehicle.start();
// This will also call the appropriate overridden method
vehicle.stop();
// Demonstrate exception handling with overridden methods
try {
vehicle.checkMaintenance();
} catch (FileNotFoundException e) {
System.out.println("File not found: " + e.getMessage());
} catch (IOException e) {
System.out.println("IO error: " + e.getMessage());
} catch (ClassNotFoundException e) {
System.out.println("Class not found: " + e.getMessage());
}
// Demonstrate covariant return types
Vehicle returnedVehicle = vehicle.getVehicle();
System.out.println("Returned vehicle type: " + returnedVehicle.getClass().getSimpleName());
// Use instanceof to access subclass-specific methods
if (vehicle instanceof Car) {
Car car = (Car) vehicle;
System.out.println("Number of doors: " + car.getNumDoors());
car.honk();
if (car instanceof ElectricCar) {
ElectricCar electricCar = (ElectricCar) car;
System.out.println("Battery capacity: " + electricCar.getBatteryCapacity() + " kWh");
electricCar.charge();
}
} else if (vehicle instanceof Motorcycle) {
Motorcycle motorcycle = (Motorcycle) vehicle;
System.out.println("Has sidecar: " + motorcycle.hasSidecar());
motorcycle.wheelie();
}
} catch (IOException e) {
System.out.println("Error starting vehicle: " + e.getMessage());
}
System.out.println("-----------------------------------");
}
}
}
Code Explanation
This example demonstrates various aspects of method overriding:
-
Abstract Method Overriding:
Vehicle.start()
is an abstract method that must be implemented by all subclassesCar
,ElectricCar
, andMotorcycle
all provide their own implementations
-
Regular Method Overriding:
Vehicle.stop()
is a regular method that is overridden inCar
ElectricCar
inherits the overridden version fromCar
-
Final Method:
Vehicle.getInfo()
is a final method that cannot be overridden
-
Covariant Return Types:
Vehicle.getVehicle()
returns aVehicle
Car.getVehicle()
overrides it to return aCar
(a subtype ofVehicle
)
-
Exception Handling:
Vehicle.checkMaintenance()
throwsIOException
andClassNotFoundException
Car.checkMaintenance()
overrides it to throw onlyFileNotFoundException
(a subtype ofIOException
)ElectricCar.checkMaintenance()
overrides it to throw no exceptions
-
Access Modifiers:
Vehicle.performMaintenance()
isprotected
Car.performMaintenance()
overrides it aspublic
(less restrictive)
-
Polymorphism:
- The
main
method demonstrates polymorphism by calling methods onVehicle
references - The actual method executed depends on the runtime type of the object
- The
⚠️ Common Pitfalls in Java Method Overriding
1. Confusing Overriding with Overloading
One of the most common mistakes is confusing method overriding with method overloading:
class Parent {
public void display(String message) {
System.out.println("Parent: " + message);
}
}
class Child extends Parent {
// This is overloading, not overriding!
public void display(String message, String prefix) {
System.out.println(prefix + " Child: " + message);
}
// This is overriding
@Override
public void display(String message) {
System.out.println("Child: " + message);
}
}
2. Forgetting the @Override
Annotation
While not required, omitting the @Override
annotation can lead to subtle bugs:
class Parent {
public void displayMessage() {
System.out.println("Parent message");
}
}
class Child extends Parent {
// Typo in method name - this is a new method, not an override
public void displayMesssage() { // Notice the extra 's'
System.out.println("Child message");
}
}
With the @Override
annotation, the compiler would catch this error:
class Child extends Parent {
@Override
public void displayMesssage() { // Compiler error: method does not override a method from its superclass
System.out.println("Child message");
}
}
3. Violating the Rules of Exception Handling
Throwing broader or new checked exceptions in an overriding method is a common mistake:
class Parent {
public void process() throws IOException {
// Implementation
}
}
class Child extends Parent {
@Override
public void process() throws Exception { // Compilation error: broader exception
// Implementation
}
}
4. Misunderstanding Method Hiding vs. Overriding
Static methods cannot be overridden; they can only be hidden:
class Parent {
public static void staticMethod() {
System.out.println("Parent's static method");
}
}
class Child extends Parent {
// This is method hiding, not overriding
public static void staticMethod() {
System.out.println("Child's static method");
}
}
The key difference is that method overriding is resolved at runtime based on the object's type, while method hiding is resolved at compile time based on the reference type:
Parent p = new Child();
p.staticMethod(); // Calls Parent's staticMethod
Child c = new Child();
c.staticMethod(); // Calls Child's staticMethod
5. Attempting to Override Private Methods
Private methods are not visible to subclasses and cannot be overridden:
class Parent {
private void privateMethod() {
System.out.println("Parent's private method");
}
public void callPrivate() {
privateMethod();
}
}
class Child extends Parent {
// This is a new method, not an override
private void privateMethod() {
System.out.println("Child's private method");
}
}
When you call callPrivate()
on a Child
object, it will still call Parent
's privateMethod()
.
6. Changing the Return Type Incorrectly
The return type in the overriding method must be compatible with the return type in the superclass method:
class Parent {
public Number getValue() {
return 10;
}
}
class Child extends Parent {
@Override
public Integer getValue() { // Valid: Integer is a subclass of Number
return 20;
}
}
class AnotherChild extends Parent {
@Override
public String getValue() { // Compilation error: String is not a subclass of Number
return "20";
}
}
7. Reducing Visibility in the Overriding Method
You cannot reduce the visibility of an overridden method:
class Parent {
public void display() {
System.out.println("Parent display");
}
}
class Child extends Parent {
@Override
protected void display() { // Compilation error: cannot reduce visibility
System.out.println("Child display");
}
}
🏆 Best Practices for Java Method Overriding
1. Always Use the @Override
Annotation
Always use the @Override
annotation when overriding a method:
class Child extends Parent {
@Override
public void display() {
System.out.println("Child display");
}
}
This helps catch errors at compile time and makes your code more readable.
2. Follow the Liskov Substitution Principle
The Liskov Substitution Principle (LSP) states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. When overriding methods, ensure that:
- Preconditions cannot be strengthened in the subclass
- Postconditions cannot be weakened in the subclass
- Invariants must be preserved
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
// This violates LSP
class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width); // A square must have equal sides
}
@Override
public void setHeight(int height) {
super.setHeight(height);
super.setWidth(height); // A square must have equal sides
}
}
This violates LSP because code that works with Rectangle
might not work correctly with Square
.
3. Don't Change the Expected Behavior
When overriding a method, don't change its expected behavior drastically:
class Parent {
public boolean isPositive(int number) {
return number > 0;
}
}
// Bad practice
class Child extends Parent {
@Override
public boolean isPositive(int number) {
return number >= 0; // Changed behavior: zero is now considered positive
}
}
4. Consider Using super
to Extend Behavior
Instead of completely replacing the parent's implementation, consider extending it:
class Parent {
public void process() {
System.out.println("Parent processing");
}
}
class Child extends Parent {
@Override
public void process() {
super.process(); // Call parent's implementation
System.out.println("Child additional processing");
}
}
5. Be Careful with Constructors
Constructors are not inherited and cannot be overridden. Always ensure proper constructor chaining:
class Parent {
private String name;
public Parent(String name) {
this.name = name;
}
}
class Child extends Parent {
private int age;
public Child(String name, int age) {
super(name); // Call parent constructor
this.age = age;
}
}
6. Document Overridden Methods
Clearly document any changes in behavior when overriding methods:
class Child extends Parent {
/**
* {@inheritDoc}
* <p>
* This implementation adds additional validation for negative numbers.
* </p>
*/
@Override
public void process(int number) {
if (number < 0) {
throw new IllegalArgumentException("Number cannot be negative");
}
super.process(number);
}
}
7. Be Consistent with Exception Handling
When overriding methods that throw exceptions, be consistent and follow the rules:
class Parent {
public void process() throws IOException {
// Implementation
}
}
// Good practice
class Child extends Parent {
@Override
public void process() throws FileNotFoundException { // Narrower exception
// Implementation
}
}
8. Use Covariant Return Types Appropriately
Use covariant return types when it makes sense semantically:
class Parent {
public Number getValue() {
return 10;
}
}
// Good practice
class Child extends Parent {
@Override
public Integer getValue() { // More specific return type
return 20;
}
}
🌐 Why Method Overriding Matters in Java
1. Polymorphism
Method overriding is essential for polymorphism, one of the four pillars of object-oriented programming. It allows objects of different classes to be treated as objects of a common superclass, while each providing its own implementation of methods:
// Polymorphism in action
Shape shape1 = new Circle(5);
Shape shape2 = new Rectangle(4, 6);
// The correct implementation is called based on the actual object type
System.out.println(shape1.calculateArea()); // Uses Circle's implementation
System.out.println(shape2.calculateArea()); // Uses Rectangle's implementation
2. Code Reusability
Method overriding promotes code reusability by allowing subclasses to inherit and customize behavior:
class UIComponent {
public void render() {
// Common rendering logic
System.out.println("Preparing to render component");
doRender(); // This will be overridden by subclasses
System.out.println("Component rendered");
}
protected void doRender() {
// Default implementation
}
}
class Button extends UIComponent {
@Override
protected void doRender() {
System.out.println("Rendering a button");
}
}
class TextField extends UIComponent {
@Override
protected void doRender() {
System.out.println("Rendering a text field");
}
}
3. Extensibility
Method overriding makes your code more extensible by allowing new subclasses to provide specialized behavior without modifying existing code:
// Original code
abstract class PaymentProcessor {
public abstract void processPayment(double amount);
}
class CreditCardProcessor extends PaymentProcessor {
@Override
public void processPayment(double amount) {
System.out.println("Processing credit card payment of $" + amount);
}
}
// Later, you can add new payment methods without changing existing code
class PayPalProcessor extends PaymentProcessor {
@Override
public void processPayment(double amount) {
System.out.println("Processing PayPal payment of $" + amount);
}
}
4. Framework and Library Design
Method overriding is crucial for designing frameworks and libraries that can be extended by users:
// Framework code
public abstract class HttpServlet {
public final void service(Request request, Response response) {
// Common processing
// Dispatch to appropriate method based on HTTP method
if (request.getMethod().equals("GET")) {
doGet(request, response);
} else if (request.getMethod().equals("POST")) {
doPost(request, response);
}
// ...
}
// Default implementations that can be overridden
protected void doGet(Request request, Response response) {
response.sendError(405, "Method Not Allowed");
}
protected void doPost(Request request, Response response) {
response.sendError(405, "Method Not Allowed");
}
}
// User code
public class MyServlet extends HttpServlet {
@Override
protected void doGet(Request request, Response response) {
response.setContentType("text/html");
response.getWriter().println("<h1>Hello, World!</h1>");
}
}
5. Template Method Pattern
Method overriding is the foundation of the Template Method design pattern, which defines the skeleton of an algorithm in a method, deferring some steps to subclasses:
abstract class DataProcessor {
// Template method
public final void processData() {
readData();
validateData();
transformData();
writeData();
}
// Steps that can be overridden by subclasses
protected abstract void readData();
protected abstract void validateData();
protected void transformData() {
// Default implementation
}
protected abstract void writeData();
}
class FileDataProcessor extends DataProcessor {
@Override
protected void readData() {
System.out.println("Reading data from file");
}
@Override
protected void validateData() {
System.out.println("Validating file data");
}
@Override
protected void writeData() {
System.out.println("Writing processed data to file");
}
}
📝 Exercises and Mini-Projects
Let's put your knowledge of method overriding into practice with some exercises and mini-projects.
Exercise 1: Shape Hierarchy
Task: Create a shape hierarchy with a base Shape
class and various subclasses (Circle
, Rectangle
, Triangle
). Implement method overriding for calculating area and perimeter.
Requirements:
- Create an abstract
Shape
class with abstract methods for calculating area and perimeter - Implement concrete subclasses for different shapes
- Override the methods appropriately in each subclass
- Create a test program that demonstrates polymorphism
Too see full example, Click to expand!
Solution:
// Abstract base class
abstract class Shape {
// Abstract methods to be overridden by subclasses
public abstract double calculateArea();
public abstract double calculatePerimeter();
// Common method for all shapes
public void display() {
System.out.println("This is a " + getClass().getSimpleName());
System.out.println("Area: " + calculateArea());
System.out.println("Perimeter: " + calculatePerimeter());
}
}
// Circle implementation
class Circle extends Shape {
private double radius;
public Circle(double radius) {
if (radius <= 0) {
throw new IllegalArgumentException("Radius must be positive");
}
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius
}
@Override
public double calculatePerimeter() {
return 2 * Math.PI * radius;
}
public double getRadius() {
return radius;
}
}
// Rectangle implementation
class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Width and height must be positive");
}
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
@Override
public double calculatePerimeter() {
return 2 * (width + height);
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
}
// Triangle implementation
class Triangle extends Shape {
private double sideA;
private double sideB;
private double sideC;
public Triangle(double sideA, double sideB, double sideC) {
// Check if the sides can form a valid triangle
if (sideA <= 0 || sideB <= 0 || sideC <= 0) {
throw new IllegalArgumentException("All sides must be positive");
}
if (sideA + sideB <= sideC || sideA + sideC <= sideB || sideB + sideC <= sideA) {
throw new IllegalArgumentException("The sides do not form a valid triangle");
}
this.sideA = sideA;
this.sideB = sideB;
this.sideC = sideC;
}
@Override
public double calculateArea() {
// Using Heron's formula
double s = (sideA + sideB + sideC) / 2;
return Math.sqrt(s * (s - sideA) * (s - sideB) * (s - sideC));
}
@Override
public double calculatePerimeter() {
return sideA + sideB + sideC;
}
public double getSideA() {
return sideA;
}
public double getSideB() {
return sideB;
}
public double getSideC() {
return sideC;
}
}
// Test program
class ShapeTest {
public static void main(String[] args) {
// Create an array of shapes
Shape[] shapes = new Shape[3];
shapes[0] = new Circle(5);
shapes[1] = new Rectangle(4, 6);
shapes[2] = new Triangle(3, 4, 5);
// Demonstrate polymorphism
for (Shape shape : shapes) {
System.out.println("\n----- Shape Information -----");
shape.display();
// Demonstrate instanceof and casting
if (shape instanceof Circle) {
Circle circle = (Circle) shape;
System.out.println("This is a circle with radius: " + circle.getRadius());
} else if (shape instanceof Rectangle) {
Rectangle rectangle = (Rectangle) shape;
System.out.println("This is a rectangle with width: " + rectangle.getWidth() +
" and height: " + rectangle.getHeight());
} else if (shape instanceof Triangle) {
Triangle triangle = (Triangle) shape;
System.out.println("This is a triangle with sides: " +
triangle.getSideA() + ", " +
triangle.getSideB() + ", " +
triangle.getSideC());
}
}
}
}
Your turn: Try implementing a similar hierarchy for different types of vehicles (e.g., Car
, Motorcycle
, Truck
) with methods like calculateFuelEfficiency()
and getMaxSpeed()
.
Exercise 2: Banking System
Task: Create a banking system with different types of accounts that override methods for interest calculation and transaction processing.
Requirements:
- Create a base
Account
class with methods for deposit, withdraw, and calculate interest - Implement subclasses for different account types (e.g.,
SavingsAccount
,CheckingAccount
,FixedDepositAccount
) - Override the methods appropriately in each subclass
- Create a test program that demonstrates the different behaviors
Too see full example, Click to expand!
Solution:
// Base Account class
abstract class Account {
private String accountNumber;
private String accountHolder;
protected double balance; // Protected to allow direct access in subclasses
public Account(String accountNumber, String accountHolder, double initialBalance) {
if (initialBalance < 0) {
throw new IllegalArgumentException("Initial balance cannot be negative");
}
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
this.balance = initialBalance;
}
// Method to deposit money
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Deposit amount must be positive");
}
balance += amount;
System.out.println("Deposited: $" + amount);
System.out.println("New balance: $" + balance);
}
// Method to withdraw money - can be overridden
public void withdraw(double amount) throws InsufficientFundsException {
if (amount <= 0) {
throw new IllegalArgumentException("Withdrawal amount must be positive");
}
if (amount > balance) {
throw new InsufficientFundsException("Insufficient funds. Current balance: $" + balance);
}
balance -= amount;
System.out.println("Withdrawn: $" + amount);
System.out.println("New balance: $" + balance);
}
// Abstract method to calculate interest - must be implemented by subclasses
public abstract double calculateInterest();
// Method to apply interest to the account
public void applyInterest() {
double interest = calculateInterest();
balance += interest;
System.out.println("Interest applied: $" + interest);
System.out.println("New balance: $" + balance);
}
// Method to display account information
public void displayInfo() {
System.out.println("Account Number: " + accountNumber);
System.out.println("Account Holder: " + accountHolder);
System.out.println("Balance: $" + balance);
System.out.println("Account Type: " + getClass().getSimpleName());
}
// Getters
public String getAccountNumber() {
return accountNumber;
}
public String getAccountHolder() {
return accountHolder;
}
public double getBalance() {
return balance;
}
}
// Custom exception for insufficient funds
class InsufficientFundsException extends Exception {
public InsufficientFundsException(String message) {
super(message);
}
}
// Savings Account implementation
class SavingsAccount extends Account {
private double interestRate;
private int withdrawalsThisMonth;
private final int FREE_WITHDRAWALS_LIMIT = 3;
private final double WITHDRAWAL_FEE = 2.0;
public SavingsAccount(String accountNumber, String accountHolder, double initialBalance, double interestRate) {
super(accountNumber, accountHolder, initialBalance);
if (interestRate < 0) {
throw new IllegalArgumentException("Interest rate cannot be negative");
}
this.interestRate = interestRate;
this.withdrawalsThisMonth = 0;
}
@Override
public void withdraw(double amount) throws InsufficientFundsException {
withdrawalsThisMonth++;
// Apply withdrawal fee if exceeded free withdrawals limit
if (withdrawalsThisMonth > FREE_WITHDRAWALS_LIMIT) {
double totalAmount = amount + WITHDRAWAL_FEE;
if (totalAmount > balance) {
throw new InsufficientFundsException("Insufficient funds including fee. Current balance: $" + balance);
}
super.withdraw(amount);
balance -= WITHDRAWAL_FEE;
System.out.println("Withdrawal fee applied: $" + WITHDRAWAL_FEE);
System.out.println("New balance after fee: $" + balance);
} else {
super.withdraw(amount);
}
}
@Override
public double calculateInterest() {
return balance * interestRate / 12; // Monthly interest
}
public void resetWithdrawals() {
withdrawalsThisMonth = 0;
System.out.println("Monthly withdrawal count reset for account: " + getAccountNumber());
}
public double getInterestRate() {
return interestRate;
}
public int getWithdrawalsThisMonth() {
return withdrawalsThisMonth;
}
}
// Checking Account implementation
class CheckingAccount extends Account {
private double overdraftLimit;
private final double MONTHLY_FEE = 5.0;
private final double INTEREST_RATE = 0.001; // 0.1% annual interest
public CheckingAccount(String accountNumber, String accountHolder, double initialBalance, double overdraftLimit) {
super(accountNumber, accountHolder, initialBalance);
if (overdraftLimit < 0) {
throw new IllegalArgumentException("Overdraft limit cannot be negative");
}
this.overdraftLimit = overdraftLimit;
}
@Override
public void withdraw(double amount) throws InsufficientFundsException {
if (amount <= 0) {
throw new IllegalArgumentException("Withdrawal amount must be positive");
}
// Allow withdrawal up to balance + overdraft limit
if (amount > balance + overdraftLimit) {
throw new InsufficientFundsException("Exceeds overdraft limit. Available funds: $" +
(balance + overdraftLimit));
}
balance -= amount;
System.out.println("Withdrawn: $" + amount);
if (balance < 0) {
System.out.println("Account is in overdraft. Current balance: $" + balance);
} else {
System.out.println("New balance: $" + balance);
}
}
@Override
public double calculateInterest() {
// Only apply interest if balance is positive
return balance > 0 ? balance * INTEREST_RATE / 12 : 0;
}
public void applyMonthlyFee() {
balance -= MONTHLY_FEE;
System.out.println("Monthly fee applied: $" + MONTHLY_FEE);
System.out.println("New balance: $" + balance);
}
public double getOverdraftLimit() {
return overdraftLimit;
}
}
// Fixed Deposit Account implementation
class FixedDepositAccount extends Account {
private double interestRate;
private int termMonths;
private boolean matured;
private final double EARLY_WITHDRAWAL_PENALTY = 0.05; // 5% penalty
public FixedDepositAccount(String accountNumber, String accountHolder, double initialBalance,
double interestRate, int termMonths) {
super(accountNumber, accountHolder, initialBalance);
if (interestRate < 0) {
throw new IllegalArgumentException("Interest rate cannot be negative");
}
if (termMonths <= 0) {
throw new IllegalArgumentException("Term must be positive");
}
this.interestRate = interestRate;
this.termMonths = termMonths;
this.matured = false;
}
@Override
public void withdraw(double amount) throws InsufficientFundsException {
if (!matured) {
System.out.println("Warning: Early withdrawal before maturity will incur a penalty.");
double penalty = amount * EARLY_WITHDRAWAL_PENALTY;
double totalAmount = amount + penalty;
if (totalAmount > balance) {
throw new InsufficientFundsException("Insufficient funds including penalty. Current balance: $" + balance);
}
super.withdraw(amount);
balance -= penalty;
System.out.println("Early withdrawal penalty applied: $" + penalty);
System.out.println("New balance after penalty: $" + balance);
} else {
super.withdraw(amount);
}
}
@Override
public double calculateInterest() {
return balance * interestRate / 12; // Monthly interest
}
public void mature() {
matured = true;
System.out.println("Account has matured. No early withdrawal penalties will apply.");
}
public boolean isMatured() {
return matured;
}
public double getInterestRate() {
return interestRate;
}
public int getTermMonths() {
return termMonths;
}
}
// Test program
class BankingSystemTest {
public static void main(String[] args) {
// Create different types of accounts
try {
SavingsAccount savings = new SavingsAccount("SA001", "John Doe", 1000, 0.05);
CheckingAccount checking = new CheckingAccount("CA001", "Jane Smith", 2000, 500);
FixedDepositAccount fixedDeposit = new FixedDepositAccount("FD001", "Bob Johnson", 5000, 0.08, 12);
System.out.println("===== Account Information =====");
savings.displayInfo();
System.out.println("\n");
checking.displayInfo();
System.out.println("\n");
fixedDeposit.displayInfo();
System.out.println("\n===== Deposit Operations =====");
savings.deposit(500);
checking.deposit(1000);
System.out.println("\n===== Withdrawal Operations =====");
try {
savings.withdraw(200);
savings.withdraw(100);
savings.withdraw(50);
savings.withdraw(50); // This should apply a fee
} catch (InsufficientFundsException e) {
System.out.println("Error: " + e.getMessage());
}
try {
checking.withdraw(2500); // This should use the overdraft
} catch (InsufficientFundsException e) {
System.out.println("Error: " + e.getMessage());
}
try {
fixedDeposit.withdraw(1000); // This should apply a penalty
} catch (InsufficientFundsException e) {
System.out.println("Error: " + e.getMessage());
}
System.out.println("\n===== Interest Calculations =====");
System.out.println("Savings Interest: $" + savings.calculateInterest());
System.out.println("Checking Interest: $" + checking.calculateInterest());
System.out.println("Fixed Deposit Interest: $" + fixedDeposit.calculateInterest());
System.out.println("\n===== Applying Interest =====");
savings.applyInterest();
checking.applyInterest();
fixedDeposit.applyInterest();
System.out.println("\n===== Monthly Maintenance =====");
savings.resetWithdrawals();
checking.applyMonthlyFee();
System.out.println("\n===== Fixed Deposit Maturity =====");
fixedDeposit.mature();
try {
fixedDeposit.withdraw(1000); // No penalty should apply now
} catch (InsufficientFundsException e) {
System.out.println("Error: " + e.getMessage());
}
} catch (IllegalArgumentException e) {
System.out.println("Error creating accounts: " + e.getMessage());
}
}
}
Your turn: Try extending this banking system by adding more account types (e.g., StudentAccount
, BusinessAccount
) with their own specific behaviors and overridden methods.
Exercise 3: Employee Management System
Task: Create an employee management system with different types of employees that override methods for calculating salary and benefits.
Requirements:
- Create a base
Employee
class with methods for calculating salary, benefits, and displaying information - Implement subclasses for different employee types (e.g.,
FullTimeEmployee
,PartTimeEmployee
,Contractor
) - Override the methods appropriately in each subclass
- Create a test program that demonstrates the different behaviors
Your turn: Try implementing this employee management system on your own before looking at a solution.
🎯 Key Takeaways: Mastering Method Overriding in Java
Let's summarize the key points about method overriding in Java:
-
Definition: Method overriding allows a subclass to provide a specific implementation of a method that is already defined in its parent class.
-
Rules:
- The method must have the same name and parameter list
- The return type must be the same or a subtype (covariant return)
- The access modifier cannot be more restrictive
- The method cannot throw broader or new checked exceptions
- Final, static, and private methods cannot be overridden
-
Benefits:
- Enables runtime polymorphism
- Promotes code reusability
- Enhances extensibility
- Supports the "is-a" relationship in inheritance
-
Best Practices:
- Always use the
@Override
annotation - Follow the Liskov Substitution Principle
- Don't change the expected behavior drastically
- Consider using
super
to extend behavior - Be consistent with exception handling
- Document overridden methods
- Always use the
-
Common Pitfalls:
- Confusing overriding with overloading
- Forgetting the
@Override
annotation - Violating exception handling rules
- Misunderstanding method hiding vs. overriding
- Attempting to override private methods
- Changing the return type incorrectly
- Reducing visibility in the overriding method
Method overriding is a powerful feature that, when used correctly, can make your code more flexible, maintainable, and extensible. By understanding the rules and best practices, you can leverage method overriding to create well-designed object-oriented systems.
Remember, the best way to master method overriding is through practice. Try to implement the exercises and mini-projects provided in this tutorial, and then create your own projects that leverage method overriding to solve real-world problems.
Happy coding! 🚀