☕ Java

Single Inheritance

Single inheritance is the most fundamental relationship between classes in Java — one class extends exactly one other class, inheriting its fields and methods. The extends keyword establishes an is-a relationship: a Dog is an Animal, a Manager is an Employee, a Circle is a Shape. Java deliberately restricts each class to one direct superclass, avoiding the ambiguity and complexity of multiple class inheritance. Understanding single inheritance means understanding the superclass relationship, the super keyword, method overriding, constructor chaining up the hierarchy, and the Liskov Substitution Principle that makes inheritance safe and predictable.

The extends Keyword and the Is-A Relationship

Single inheritance is declared with the extends keyword. The class being extended is the superclass (also called parent class or base class). The extending class is the subclass (also called child class or derived class). The subclass inherits everything that is not private from the superclass — fields, methods, and nested types — and can add new members or override inherited ones. The is-a relationship is the litmus test for whether inheritance is the right tool. Before writing extends, ask: is a subclass instance truly an instance of the superclass type in every circumstance? A Dog is always an Animal — passing a Dog anywhere an Animal is expected always makes sense. A Manager is always an Employee. These relationships hold universally, which is what makes them genuine is-a relationships appropriate for inheritance. When the is-a test fails, composition is almost always the right answer instead. A Car has an Engine — it is not an Engine. A Stack can be implemented using an ArrayList — a Stack is not an ArrayList (violating is-a), it has a list as its internal storage. Java's own library made this mistake with Stack extending Vector — Stack inherited dozens of methods (insertElementAt, setElementAt, remove(int)) that make no sense for a stack and violate the abstraction. Every class that does not explicitly extend another class implicitly extends java.lang.Object. Object is therefore the root of every class hierarchy in Java, and every object supports the Object methods: toString(), equals(), hashCode(), getClass(), and the thread synchronisation methods.
Java
// ── Single inheritance declaration ───────────────────────────────────
public class Animal {

    private final String name;
    private final String species;
    protected     int    age;         // protected — subclasses can read

    public Animal(String name, String species) {
        this.name    = name;
        this.species = species;
        this.age     = 0;
    }

    public void eat()   { System.out.println(name + " is eating."); }
    public void sleep() { System.out.println(name + " is sleeping."); }

    public String getName()    { return name;    }
    public String getSpecies() { return species; }
    public int    getAge()     { return age;     }

    @Override
    public String toString() {
        return species + " named " + name + " (age " + age + ")";
    }
}

// ── Dog IS-A Animal — all Animal behaviour is inherited ───────────────
public class Dog extends Animal {

    private final String breed;
    private       boolean trained;

    public Dog(String name, String breed) {
        super(name, "Canis lupus familiaris");  // call Animal constructor
        this.breed   = breed;
        this.trained = false;
    }

    // ── Dog ADDS new behaviour not in Animal ──────────────────────────
    public void fetch() {
        System.out.println(getName() + " fetches the ball!");
    }

    public void train() {
        this.trained = true;
        System.out.println(getName() + " has been trained.");
    }

    public String  getBreed()   { return breed;   }
    public boolean isTrained()  { return trained; }

    // ── Is-a in action — Dog used wherever Animal is expected ─────────
    public static void makeAnimalSleep(Animal a) {
        a.sleep();   // works for Dog, Cat, Bird — anything that IS-A Animal
    }
}

// ── Polymorphic usage ─────────────────────────────────────────────────
Dog    dog  = new Dog("Rex", "Labrador");
Animal a    = dog;             // Dog IS-A Animal — widening is automatic

dog.fetch();                   // Dog-specific method
dog.eat();                     // inherited from Animal
a.eat();                       // same object, same inherited method

System.out.println(a instanceof Dog);     // true
System.out.println(a instanceof Animal);  // true
System.out.println(a instanceof Object);  // true — always

The super Keyword

The super keyword provides explicit access to the superclass portion of an object. It has two distinct uses. In a constructor, super() calls the superclass constructor and must be the very first statement in the constructor body. The compiler inserts an implicit super() with no arguments if the first statement is not already a super() or this() call — which means if the superclass has no no-argument constructor, every subclass constructor must explicitly call super() with the required arguments or the code will not compile. In methods, super.methodName() calls the superclass version of a method. This is essential when a subclass overrides a method but wants to extend rather than replace the superclass behaviour — it calls super.describe() first and then adds additional information. Without this ability, overriding would always mean completely replacing, making it difficult to build on top of existing behaviour. The super keyword cannot be chained — you cannot write super.super.method() to call a grandparent's method. This is intentional. A class knows only about its direct superclass. If grandparent behaviour is needed, the parent class should expose it through its own API. This restriction keeps class hierarchies from becoming tangled with skip-level dependencies.
Java
// ── super() in constructors — chaining up the hierarchy ─────────────
public class Vehicle {

    private final String make;
    private final String model;
    private final int    year;

    public Vehicle(String make, String model, int year) {
        if (year < 1886 || year > 2100) {
            throw new IllegalArgumentException(
                "Invalid year: " + year);
        }
        this.make  = make;
        this.model = model;
        this.year  = year;
    }

    public String describe() {
        return year + " " + make + " " + model;
    }

    public String getMake()  { return make;  }
    public String getModel() { return model; }
    public int    getYear()  { return year;  }
}

public class Car extends Vehicle {

    private final int   doors;
    private       double fuelLevel;

    public Car(String make, String model, int year, int doors) {
        super(make, model, year);   // MUST be first — initialises Vehicle
        this.doors     = doors;
        this.fuelLevel = 0.0;
    }

    // ── super.method() — extend, not replace ──────────────────────────
    @Override
    public String describe() {
        // Reuse superclass description, then add Car-specific detail
        return super.describe() + ", " + doors + "-door";
    }

    public void refuel(double litres) {
        this.fuelLevel = Math.min(60.0, fuelLevel + litres);
    }

    public double getFuelLevel() { return fuelLevel; }
    public int    getDoors()     { return doors;     }
}

public class ElectricCar extends Car {

    private final int batteryCapacityKwh;
    private       int chargePercent;

    public ElectricCar(String make, String model, int year,
                       int doors, int batteryCapacityKwh) {
        super(make, model, year, doors);  // calls Car constructor
        this.batteryCapacityKwh = batteryCapacityKwh;
        this.chargePercent      = 80;
    }

    @Override
    public String describe() {
        // super.describe() → calls Car.describe()
        // which itself calls super.describe() → Vehicle.describe()
        return super.describe() + ", electric " +
            batteryCapacityKwh + "kWh";
    }

    public int getCharge() { return chargePercent; }
}

// ── describe() outputs at each level ─────────────────────────────────
Vehicle v   = new Vehicle("Ford",  "Model T",  1908);
Car     c   = new Car("Toyota", "Camry",  2024, 4);
ElectricCar e = new ElectricCar("Tesla", "Model S", 2024, 4, 100);

System.out.println(v.describe());  // 1908 Ford Model T
System.out.println(c.describe());  // 2024 Toyota Camry, 4-door
System.out.println(e.describe());  // 2024 Tesla Model S, 4-door, electric 100kWh

Method Overriding

Method overriding is the mechanism by which a subclass provides its own implementation of a method defined in the superclass. The overriding method must have the same name, the same parameter list, and a return type that is the same as or a covariant subtype of the superclass method's return type. The access modifier can be the same or wider but never narrower — a public superclass method must be overridden with a public method. The @Override annotation does not change the program's behaviour — overriding happens whether or not the annotation is present. But the annotation provides crucial safety: if the method signature does not actually override anything (a typo in the method name, a wrong parameter type), the compiler reports an error. Without @Override, the typo creates a new overloaded method instead of an override, which compiles silently and produces mysterious runtime behaviour. Always use @Override on every method intended as an override. Dynamic dispatch — also called virtual method invocation or late binding — is what makes overriding powerful. When a method is called on a reference variable, Java determines at runtime which actual class the object is, and calls that class's version of the method. The declared type of the reference variable is irrelevant — only the actual runtime type matters. This is the mechanism behind polymorphism: a collection of Animal references can contain Dogs, Cats, and Birds, and calling sound() on each invokes the right implementation automatically.
Java
// ── Method overriding and dynamic dispatch ───────────────────────────
public abstract class Shape {

    private String colour;

    public Shape(String colour) {
        this.colour = colour;
    }

    // ── These will be overridden by each subclass ──────────────────────
    public abstract double area();
    public abstract double perimeter();

    // ── Non-abstract — subclasses may or may not override ─────────────
    public String describe() {
        return String.format("%s [colour=%s area=%.2f perimeter=%.2f]",
            getClass().getSimpleName(), colour, area(), perimeter());
    }

    public String getColour() { return colour; }
    public void   setColour(String c) { this.colour = c; }
}

public class Circle extends Shape {

    private final double radius;

    public Circle(double radius, String colour) {
        super(colour);
        this.radius = radius;
    }

    @Override                               // compiler verifies override
    public double area() {
        return Math.PI * radius * radius;
    }

    @Override
    public double perimeter() {
        return 2 * Math.PI * radius;
    }

    public double getRadius() { return radius; }
}

public class Rectangle extends Shape {

    private final double width;
    private final double height;

    public Rectangle(double width, double height, String colour) {
        super(colour);
        this.width  = width;
        this.height = height;
    }

    @Override public double area()      { return width * height;       }
    @Override public double perimeter() { return 2 * (width + height); }

    public double getWidth()  { return width;  }
    public double getHeight() { return height; }
}

// ── Dynamic dispatch — runtime type determines which method runs ───────
List<Shape> shapes = List.of(
    new Circle(5.0,    "red"),
    new Rectangle(4.0, 6.0, "blue"),
    new Circle(3.0,    "green")
);

double totalArea = 0;
for (Shape shape : shapes) {
    System.out.println(shape.describe());  // correct method for each type
    totalArea += shape.area();             // Circle.area() or Rectangle.area()
}
System.out.printf("Total area: %.2f%n", totalArea);

// ── @Override catches signature errors ───────────────────────────────
public class Triangle extends Shape {

    @Override
    public double area() { return 0; }      // correct override

    // @Override
    // public double Area() { return 0; }   // COMPILE ERROR — no match
    //                                        // without @Override: silently
    //                                        // creates a NEW method named Area
}

The Liskov Substitution Principle

The Liskov Substitution Principle (LSP) states that objects of a subclass must be substitutable for objects of the superclass without altering the correctness of the program. This is the formal statement of what the is-a relationship actually means for software. It is not enough that a Dog syntactically is-a Animal — every operation that is correct for an Animal must remain correct when performed on a Dog. LSP violations are surprisingly common and always produce incorrect or surprising behaviour. The classic example is Square extending Rectangle. Geometrically, a square is a rectangle — the is-a test seems to pass. But behaviourally it fails: setting the width of a rectangle does not change its height, but setting the width of a square must change its height too (to keep it a square). Code that holds a Rectangle reference and sets width and height independently breaks when it receives a Square. The is-a geometric relationship does not imply the is-a behavioural relationship needed for inheritance. Recognising LSP violations requires thinking about behaviour, not just structure. If a subclass must override a method to throw UnsupportedOperationException, that is an LSP violation — the subclass cannot fulfil the superclass's promise. If a subclass must weaken a precondition ("superclass requires x > 0 but subclass allows any x") that is technically allowed. If a subclass must strengthen a postcondition ("superclass promises result >= 0, subclass promises result > 0") that is also allowed. Narrowing the guarantee — promising less than the superclass — is always an LSP violation.
Java
// ── LSP VIOLATION — Square extending Rectangle ───────────────────────
public class Rectangle {

    protected double width;
    protected double height;

    public void setWidth(double width)   { this.width  = width;  }
    public void setHeight(double height) { this.height = height; }

    public double area() { return width * height; }
}

public class Square extends Rectangle {

    // Square must keep width == height — overrides both setters
    @Override
    public void setWidth(double side) {
        this.width  = side;
        this.height = side;   // also changes height!
    }

    @Override
    public void setHeight(double side) {
        this.width  = side;   // also changes width!
        this.height = side;
    }
}

// Code that works correctly for Rectangle:
public static void doubleWidth(Rectangle r) {
    r.setWidth(r.width * 2);
    // Expectation: area doubles
    // Precondition: width changes, height stays same
}

Rectangle rect   = new Rectangle();
rect.setWidth(4); rect.setHeight(3);
doubleWidth(rect);
System.out.println(rect.area());   // 24.0 — correct (48 * 3)

Square sq = new Square();
sq.setWidth(4);     // sets both to 4
doubleWidth(sq);    // sets width to 8, ALSO sets height to 8
System.out.println(sq.area());   // 64.0 — WRONG (expected 24.0)

// ── LSP-safe design: make Rectangle immutable ─────────────────────────
public final class ImmutableRectangle {

    private final double width;
    private final double height;

    public ImmutableRectangle(double width, double height) {
        this.width  = width;
        this.height = height;
    }

    public ImmutableRectangle withWidth(double w) {
        return new ImmutableRectangle(w, height);
    }

    public ImmutableRectangle withHeight(double h) {
        return new ImmutableRectangle(width, h);
    }

    public double area() { return width * height; }
}

// ── LSP violation via UnsupportedOperationException ───────────────────
// Java's own Collections.unmodifiableList returns a List that throws
// on add() and remove() — it IS-A List syntactically but NOT behaviourally
// for callers expecting a mutable List.
List<String> mutable   = new ArrayList<>();
List<String> immutable = Collections.unmodifiableList(mutable);
immutable.add("hello");   // throws UnsupportedOperationException at runtime

Inheritance vs Composition

Inheritance and composition are both mechanisms for reusing code, but they represent fundamentally different relationships with different implications. Inheritance is an is-a relationship — it binds two classes tightly at the design level and at compile time. Composition is a has-a relationship — one class contains an instance of another and delegates work to it, with a much looser coupling that can even be changed at runtime. The prefer-composition-over-inheritance guideline exists because inheritance is powerful but fragile. A subclass depends on the implementation details of the superclass — when the superclass changes its behaviour, the subclass may break even without any changes to its own code. Composition delegates to an object whose interface is stable, shielding the composed class from implementation changes. Composition also avoids the exponential proliferation of subclasses needed when behaviour must be combined in multiple ways — the decorator and strategy patterns use composition to achieve flexible combinations that would require dozens of subclasses through inheritance. The right question is not "should I use inheritance or composition?" but "does this relationship truly represent is-a, or is it merely a convenient implementation shortcut?" If it is truly is-a and the Liskov Substitution Principle holds, inheritance is correct. If the subclass is being written primarily to reuse code rather than to model a genuine type relationship, composition is the right tool.
Java
// ── Inheritance — correct is-a relationship ──────────────────────────
// A SavingsAccount IS-A BankAccount — every operation valid for
// BankAccount is valid for SavingsAccount (plus interest accrual)
public class BankAccount {
    protected double balance;
    public void deposit(double amount) { balance += amount; }
    public void withdraw(double amount) {
        if (amount > balance) throw new InsufficientFundsException();
        balance -= amount;
    }
    public double getBalance() { return balance; }
}

public class SavingsAccount extends BankAccount {
    private double interestRate;
    public void accrueInterest() {
        balance += balance * interestRate;
    }
    public void setInterestRate(double rate) { interestRate = rate; }
}

// ── Composition — has-a relationship ─────────────────────────────────
// A TextEditor HAS-A SpellChecker — it is not a SpellChecker
// Composition allows switching spell checker implementation at runtime
public interface SpellChecker {
    List<String> findMistakes(String text);
    String suggest(String word);
}

public class TextEditor {

    private String       content;
    private SpellChecker spellChecker;   // composed — not inherited

    public TextEditor(SpellChecker spellChecker) {
        this.content      = "";
        this.spellChecker = spellChecker;
    }

    // ── Delegation — editor asks spell checker to do its job ──────────
    public List<String> checkSpelling() {
        return spellChecker.findMistakes(content);
    }

    // ── Runtime flexibility — swap implementation ─────────────────────
    public void setSpellChecker(SpellChecker checker) {
        this.spellChecker = checker;
    }

    public void type(String text) { content += text; }
    public String getContent()    { return content;  }
}

// ── Stack: wrong (inheritance) vs right (composition) ─────────────────
// WRONG: Stack extends ArrayList — Stack IS-NOT-A ArrayList
// (exposes add(int, E), remove(int), set(int, E) etc — LIFO violated)
class BadStack<E> extends ArrayList<E> {
    public void push(E e) { add(e); }
    public E    pop()     { return remove(size() - 1); }
    // but callers can also call: badStack.add(0, item) — bypasses LIFO!
}

// CORRECT: Stack HAS-A Deque for storage
class GoodStack<E> {
    private final Deque<E> storage = new ArrayDeque<>();
    public void    push(E e)  { storage.push(e);    }
    public E       pop()      { return storage.pop();}
    public E       peek()     { return storage.peek();}
    public boolean isEmpty()  { return storage.isEmpty(); }
    public int     size()     { return storage.size();    }
    // no add(int, E), no remove(int) — LIFO contract cannot be violated
}

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.