☕ Java
Method Overriding
Method overriding occurs when a subclass provides its own implementation of a method that is already defined in its superclass. The overriding method must have the same name, the same parameter list, and a return type that is the same or a subtype of the original. Overriding is the mechanism that makes runtime polymorphism possible — the correct implementation is selected at runtime based on the actual type of the object, not the declared type of the reference variable.
What Is Method Overriding and Why It Exists
When a class inherits from another class, it inherits all of the superclass's non-private methods. In many cases the inherited behaviour is exactly what the subclass needs and nothing further is required. But sometimes the subclass represents a specialisation of the parent concept that requires different behaviour for a specific operation. Method overriding provides the mechanism to replace the inherited behaviour with one tailored to the subclass while keeping the same method signature so that existing code using the superclass reference continues to work.
Consider a Shape hierarchy. Every shape has an area, but the formula for computing that area is fundamentally different for a Circle compared to a Rectangle or a Triangle. Defining area() in the Shape superclass with a default implementation that every subclass inherits would produce incorrect results — a Circle cannot inherit Rectangle's area formula. Overriding allows each subclass to supply the correct formula while allowing client code that holds a Shape reference to call area() without knowing or caring which specific shape it is dealing with.
This is the Open/Closed Principle in action: the Shape class and client code are closed for modification, but the system is open for extension by adding new shape subclasses that override area() with their own correct formula. No existing code changes when a Triangle class is added — the polymorphic call to area() automatically routes to the Triangle's implementation.
Without method overriding, inheritance would be limited to code reuse — subclasses could only add new methods, not replace inherited ones. Overriding transforms inheritance from a code-sharing mechanism into a behaviour-customisation mechanism, which is what makes object-oriented hierarchies genuinely useful.
Java
// ── Superclass defines the contract: ─────────────────────────────────
public class Shape {
private String colour;
public Shape(String colour) {
this.colour = colour;
}
public String getColour() { return colour; }
// Defines WHAT every shape must be able to do:
public double area() {
return 0.0; // default — subclasses should override
}
public double perimeter() {
return 0.0;
}
public String describe() {
return String.format("%s %s: area=%.2f, perimeter=%.2f",
colour, getClass().getSimpleName(), area(), perimeter());
}
}
// ── Each subclass overrides with its own correct formula: ─────────────
public class Circle extends Shape {
private final double radius;
public Circle(String colour, double radius) {
super(colour);
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius; // correct formula for circle
}
@Override
public double perimeter() {
return 2 * Math.PI * radius;
}
}
public class Rectangle extends Shape {
private final double width;
private final double height;
public Rectangle(String colour, double width, double height) {
super(colour);
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height; // correct formula for rectangle
}
@Override
public double perimeter() {
return 2 * (width + height);
}
}
// ── Polymorphic usage — correct method called automatically: ──────────
List<Shape> shapes = List.of(
new Circle("Red", 5.0),
new Rectangle("Blue", 4.0, 6.0),
new Circle("Green", 3.0)
);
for (Shape shape : shapes) {
System.out.println(shape.describe());
}
// Red Circle: area=78.54, perimeter=31.42
// Blue Rectangle: area=24.00, perimeter=20.00
// Green Circle: area=28.27, perimeter=18.85Rules of Method Overriding
Overriding has strict rules enforced by the Java compiler. Breaking any of these rules either prevents overriding from occurring (producing a compile error) or silently creates a new method that does not override the parent, leading to bugs where the expected polymorphic behaviour does not occur. Understanding all the rules is essential for writing correct class hierarchies.
The method name and parameter list must be identical to the method being overridden. Any difference in parameter types, count, or order means the subclass method is an overload rather than an override — it adds a new method rather than replacing the inherited one. This is a common source of subtle bugs: intending to override but accidentally overloading because of a parameter type difference.
The return type must be the same type or a subtype (covariant return type). This allows overriding methods to return a more specific type, which is useful in factory methods and builder patterns. The access modifier of the overriding method must be the same or more permissive than the overridden method — you can widen access (protected to public) but not narrow it (public to private). Narrowing access would allow a superclass reference to call the method, which would then fail because the subclass made it more restricted.
Methods declared as private, static, or final cannot be overridden. Private methods are not inherited and are not visible to subclasses. Static methods can be hidden (a subclass can define a static method with the same name) but hiding is entirely different from overriding — hiding is resolved at compile time based on the reference type, while overriding is resolved at runtime based on the object type. Final methods explicitly prevent overriding as a design decision.
Java
// ── Overriding rules: ────────────────────────────────────────────────
public class Animal {
// These can be overridden:
public String speak() { return "..."; }
protected String describe() { return "Animal"; }
public Animal create() { return new Animal(); }
public void eat(String food) { System.out.println("Eating: " + food); }
// These CANNOT be overridden:
private void breathe() { } // private — not inherited
public static void classify() { } // static — can only be hidden
public final void identify() { } // final — overriding blocked
}
public class Dog extends Animal {
// ── Rule 1: Same name AND same parameter list: ────────────────────
@Override
public String speak() { return "Woof!"; } // ✓ overrides
public String speak(int times) { return "Woof! ".repeat(times); }
// ↑ Different parameters — this is an OVERLOAD, not an override.
// Animal's speak() is still inherited alongside this new method.
// ── Rule 2: Covariant return type — subtype is allowed: ───────────
@Override
public Dog create() { return new Dog(); } // ✓ Dog is subtype of Animal
// ── Rule 3: Access can be widened, not narrowed: ──────────────────
@Override
public String describe() { return "Dog"; } // ✓ protected → public (wider)
// @Override
// private String speak() { return "Woof"; } // ✗ public → private (narrower)
// ── Rule 4: Cannot override private methods: ─────────────────────
// This is a NEW method in Dog, not an override of Animal's breathe():
private void breathe() { System.out.println("Dog breathing"); }
// Animal's breathe() still exists separately.
// ── Rule 5: Cannot override static methods: ──────────────────────
// This HIDES Animal.classify(), not overrides it:
public static void classify() { System.out.println("Dog class"); }
// Hiding vs overriding: resolved by reference type, not object type.
// ── @Override annotation catches mistakes: ────────────────────────
// @Override
// public String Speak() { return "Woof"; } // COMPILE ERROR — wrong name
// @Override
// public String speak(String extra) { return "Woof"; } // COMPILE ERROR — wrong params
}The @Override Annotation
The @Override annotation is placed on a method that is intended to override a superclass method. It is optional — overriding works without it — but it should be used on every overriding method without exception. Its value is entirely about catching mistakes at compile time.
When @Override is present, the compiler verifies that the annotated method does actually override a method from the superclass or implement a method from an interface. If it does not — because of a typo in the name, a wrong parameter type, or because the parent method was removed — the compiler produces an error. Without @Override, the same mistake silently creates a new method that is never called polymorphically, producing a bug that may not be discovered until runtime.
Consider the case where a superclass method is later removed or renamed during refactoring. Without @Override, the subclass method that was overriding it becomes an orphan — it compiles fine, it is never called through the polymorphic dispatch that was relied upon, and the original method is effectively un-overridden. With @Override, the compiler immediately flags the subclass method as no longer overriding anything, forcing the developer to consciously decide what to do. This is exactly the kind of safety net that separates robust production code from fragile code that breaks silently under refactoring.
The @Override annotation also documents intent. A reader seeing @Override immediately knows that this method is part of a polymorphic hierarchy and that its behaviour is the customised version of an inherited contract. Without the annotation, the reader must look up the inheritance chain to determine whether the method is new or an override.
Java
// ── @Override catches typos — saves hours of debugging: ─────────────
public class Vehicle {
public String fuelType() { return "Petrol"; }
public void startEngine() { System.out.println("Vroom"); }
}
public class ElectricCar extends Vehicle {
// WITHOUT @Override — typo creates silent bug:
public String fueltype() { return "Electric"; }
// ↑ lowercase 't' — this is a NEW method, not an override!
// Vehicle's fuelType() is still inherited and returns "Petrol".
// No compile error, no warning — the bug runs silently.
// WITH @Override — compiler catches the typo immediately:
@Override
public String fueltype() { return "Electric"; } // COMPILE ERROR
// error: method does not override or implement a method
// from a supertype
// Correct spelling with @Override:
@Override
public String fuelType() { return "Electric"; } // ✓ correct
@Override
public void startEngine() {
System.out.println("Silently accelerating");
}
}
// ── @Override catches wrong parameter types: ─────────────────────────
public class Printer {
public void print(String document) {
System.out.println("Printing: " + document);
}
}
public class ColourPrinter extends Printer {
// Intended override — accidentally used Object instead of String:
// @Override
public void print(Object document) { // overload, not override!
System.out.println("Colour printing: " + document);
}
// Without @Override: compiles silently, polymorphism broken.
@Override
public void print(Object document) { // COMPILE ERROR with @Override
}
// Correct:
@Override
public void print(String document) { // ✓ correct type
System.out.println("Colour printing: " + document);
}
}
// ── @Override on interface method implementations: ────────────────────
public interface Drawable {
void draw();
String getColour();
}
public class Square implements Drawable {
@Override
public void draw() {
System.out.println("Drawing a square");
}
@Override
public String getColour() { return "Blue"; }
}Dynamic Method Dispatch
Dynamic method dispatch is the JVM mechanism that makes runtime polymorphism work. When an overriding method is called through a superclass reference, the JVM does not look at the declared type of the reference variable to decide which implementation to call. Instead, it looks up the actual type of the object at the time of the call and routes to that type's implementation. This lookup happens at runtime, not at compile time, which is why it is called dynamic dispatch (as opposed to static dispatch, where the decision is made at compile time).
Internally, every Java class has a virtual method table (vtable) — a table of method references for every overridable method. When the JVM needs to dispatch a method call, it looks up the vtable of the actual object's class. Overriding a method replaces the entry in the vtable with the overriding method's reference. This is why overriding actually works: the vtable entry is replaced, and any call that goes through the vtable hits the new entry.
This mechanism has an important implication: the vtable lookup happens every time an instance method is called through an interface or superclass reference. For the vast majority of Java programs this overhead is negligible. The JIT compiler goes further — it can inline frequently called overriding methods so the vtable lookup disappears entirely in the compiled native code. But understanding that dynamic dispatch exists explains why calling an instance method through a superclass reference calls the subclass's version, while accessing a field through a superclass reference accesses the superclass's field — fields do not have vtables and are resolved by reference type, not object type.
Java
// ── Dynamic dispatch — object type determines the method called: ─────
public class Base {
public String name = "Base"; // field — resolved by reference type
public String getName() { return "Base.getName()"; } // method — dynamic
}
public class Derived extends Base {
public String name = "Derived"; // hides Base.name (not override)
@Override
public String getName() { return "Derived.getName()"; } // overrides
}
Base ref = new Derived(); // reference type: Base, object type: Derived
// Field access — resolved by REFERENCE type (Base):
System.out.println(ref.name); // "Base" — field hiding, not override
// Method call — resolved by OBJECT type (Derived):
System.out.println(ref.getName()); // "Derived.getName()" — dynamic dispatch
// ── The vtable concept illustrated: ──────────────────────────────────
//
// Base vtable:
// getName() → Base.getName()
//
// Derived vtable:
// getName() → Derived.getName() ← entry replaced by override
//
// When ref.getName() is called:
// 1. JVM checks: what is the actual type of the object at ref? → Derived
// 2. JVM looks up Derived's vtable for getName()
// 3. Finds → Derived.getName()
// 4. Calls Derived.getName()
//
// The declared type of ref (Base) is irrelevant for method dispatch.
// ── Dynamic dispatch through multiple levels: ─────────────────────────
public class A {
public String message() { return "A"; }
}
public class B extends A {
@Override
public String message() { return "B"; }
}
public class C extends B {
@Override
public String message() { return "C"; }
}
A obj1 = new A(); System.out.println(obj1.message()); // A
A obj2 = new B(); System.out.println(obj2.message()); // B
A obj3 = new C(); System.out.println(obj3.message()); // C
B obj4 = new C(); System.out.println(obj4.message()); // C
// All four reference types — A, A, A, B — but the object types
// A, B, C, C determine which message() is called.Related Topics in Object-Oriented Programming
OOP Concepts
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects — self-contained units that combine data (fields) and behaviour (methods). Java is a class-based, object-oriented language where almost everything is an object. OOP provides four foundational principles: encapsulation, inheritance, polymorphism, and abstraction. Together they produce software that is modular, reusable, maintainable, and easier to reason about as systems grow in complexity.
Classes
A class is the fundamental building block of Java. It is a blueprint that defines the structure and behaviour of objects — what data each object holds (fields) and what operations it can perform (methods). Every Java program is composed of classes. Understanding how to design a class well — choosing the right access modifiers, separating state from behaviour, and writing cohesive single-responsibility classes — is the foundation of object-oriented programming in Java. This entry covers class anatomy, fields, methods, access modifiers, static vs instance members, the this keyword, and class design principles.
Objects
An object is a runtime instance of a class. Where a class is a blueprint that exists in source code, an object is a living entity that exists in memory during program execution — it has its own identity, its own state stored in its fields, and the ability to respond to method calls. Every object has three fundamental properties: identity (a unique memory address), state (the current values of its fields), and behaviour (the methods it responds to). This entry covers object identity vs equality, the Object class hierarchy, object state and mutation, method calls, toString, equals and hashCode, and the object lifecycle.
Object Creation
Object creation in Java is the process of allocating memory, initialising fields, and running constructor logic to bring an object into existence. The new keyword is the primary mechanism, but Java also provides factory methods, builder patterns, copy constructors, and object cloning. Constructors are special methods that set up the initial state — their design determines how easy or difficult the class is to use correctly. This entry covers constructors in depth, constructor overloading and chaining, copy constructors, factory methods, the builder pattern, and the difference between shallow and deep copy.