Inheritance and Polymorphism

This section documents the current Java release line, with Java 25 LTS as the reference point (no specific patch version is pinned), as published at the Java developer portal, the Java Tutorials, and the Java SE API specification — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Inheritance lets a class reuse and specialise another class’s members with extends; polymorphism lets a single reference type invoke the right subclass behaviour at run time. This page follows the dev.java "Inheritance" track and the Java Tutorials on subclasses.

extends, super, and Field Hiding

A class extends exactly one superclass (Java has single implementation inheritance) and inherits its non-private members. A subclass constructor must run a superclass constructor first: either an explicit super(…​) as its first statement, or an implicit no-arg super() the compiler inserts. See Using the Keyword super.

class Vehicle {
    protected final String name;
    protected int speed;

    Vehicle(String name) {
        this.name = name;
    }

    void describe() {
        System.out.println(name + " at " + speed + " km/h");
    }
}

class Car extends Vehicle {
    private final int doors;

    Car(String name, int doors) {
        super(name);            // must be first; runs Vehicle(String)
        this.doors = doors;
    }

    void openDoors() {
        System.out.println("opening " + doors + " doors");
    }
}

var car = new Car("Coupe", 2);
car.describe();     // inherited: "Coupe at 0 km/h"
car.openDoors();    // "opening 2 doors"

Method overriding is polymorphic: the subclass version replaces the superclass version for every call through any reference. Field hiding is not: a field access is resolved by the static type of the reference, so a subclass field with the same name as a superclass field merely shadows it. See Hiding Fields.

class Base {
    String label = "base";
    String who() { return "Base"; }
}

class Derived extends Base {
    String label = "derived";       // HIDES Base.label -- avoid this
    @Override String who() { return "Derived"; }   // OVERRIDES Base.who()
}

Base ref = new Derived();
System.out.println(ref.label);   // "base"     -- field: resolved by static type Base
System.out.println(ref.who());   // "Derived"  -- method: resolved by runtime type

The lesson: keep fields private and never redeclare an inherited field name — field hiding is almost always a bug.

Overriding vs. Overloading; @Override, final, abstract

Overriding replaces an inherited method with the same name and parameter types in a subclass. Overloading (covered on Methods and Parameters) is same-name, different-parameters within one class. Always mark an override with @Override so the compiler rejects a signature that does not actually override anything. Details: Overriding and Hiding Methods.

class Animal {
    String sound() { return "..."; }
}

class Dog extends Animal {
    @Override
    String sound() { return "woof"; }        // overrides Animal.sound()

    String sound(int times) {                // OVERLOADS -- different parameter list
        return "woof".repeat(times);
    }
}

An override may narrow the return type to a subtype — a covariant return type:

class Shape {
    Shape copy() { return new Shape(); }
}

class Circle extends Shape {
    @Override
    Circle copy() { return new Circle(); }   // covariant: Circle is-a Shape
}

final forbids further specialisation: a final method cannot be overridden, and a final class cannot be extended (java.lang.String and java.lang.Integer are final). See Writing Final Classes and Methods.

An abstract class cannot be instantiated and may declare abstract methods (no body) that concrete subclasses must implement. See Abstract Methods and Classes.

abstract class AbstractShape {
    abstract double area();                   // no body -- subclasses must provide one

    void printArea() {                        // concrete: shared by all subclasses
        System.out.printf("area = %.2f%n", area());
    }
}

final class Square extends AbstractShape {
    private final double side;
    Square(double side) { this.side = side; }

    @Override
    double area() { return side * side; }
}

new Square(3).printArea();   // area = 9.00
// new AbstractShape() { ... }  // allowed only as an anonymous subclass, not directly

Runtime Polymorphism and Casting

A variable of a supertype can hold any subtype instance (upcasting, implicit). Calling an overridden method on it dispatches to the runtime object’s version — dynamic dispatch. See Polymorphism.

Animal a = new Dog();        // upcast: implicit, always safe
System.out.println(a.sound());   // "woof" -- Dog's version, chosen at run time

Going the other way (downcasting) is explicit and checked at run time; an impossible cast throws ClassCastException. Guard every downcast with instanceof, using the pattern form so the cast and variable binding happen in one step:

Animal pet = new Dog();

if (pet instanceof Dog dog) {        // test + cast + bind
    System.out.println(dog.sound(3));   // "woofwoofwoof"
}

Animal other = new Animal();
// Dog bad = (Dog) other;            // compiles, but throws ClassCastException at run time
if (other instanceof Dog d) {
    // not entered: 'other' is a plain Animal
}

Pattern instanceof (and its use in switch) is covered in full on Pattern Matching. Prefer polymorphic method calls over instanceof chains wherever the behaviour can live on the type itself.

The diagram below shows a Shape reference at a call site dispatching to the concrete Circle.area() implementation of the object actually held:

Class hierarchy: java.lang.Object at the root; an interface Shape and an abstract class AbstractShape that implements Shape; concrete subclasses Circle and Rectangle extending AbstractShape; a call site holding a Shape reference bound to a Circle, with a dispatch arrow to Circle.area()

The Root Class: java.lang.Object

Every class extends java.lang.Object directly or transitively, inheriting a handful of methods worth overriding correctly. See Object as a Superclass.

toString() should return a concise, human-readable description; getClass() returns the exact runtime Class object and cannot be overridden.

final class Point {
    private final int x;
    private final int y;

    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public String toString() {
        return "Point[x=" + x + ", y=" + y + "]";
    }
}

var p = new Point(1, 2);
System.out.println(p);              // Point[x=1, y=2]
System.out.println(p.getClass().getName());   // Point

The equals/hashCode contract

If you override equals you must override hashCode consistently. equals must be reflexive, symmetric, transitive, consistent, and x.equals(null) must be false. hashCode must return the same value for two objects that are equals, and should stay stable while the object’s equals-relevant state does not change.

import java.util.Objects;

final class Money {
    private final long amount;
    private final String currency;

    Money(long amount, String currency) {
        this.amount = amount;
        this.currency = Objects.requireNonNull(currency, "currency");
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                       // reflexive fast path
        if (!(o instanceof Money other)) return false;    // type check + null check
        return amount == other.amount
            && currency.equals(other.currency);
    }

    @Override
    public int hashCode() {
        return Objects.hash(amount, currency);            // same fields as equals()
    }

    @Override
    public String toString() {
        return amount + " " + currency;
    }
}

For a plain immutable data carrier like this, prefer a record, which generates a correct equals/hashCode/toString for you — see Records and Sealed Classes.

clone and Cloneable are discouraged

Object.clone() with Cloneable is a long-standing misfeature: Cloneable has no clone method, clone bypasses constructors, and it interacts badly with final fields. Prefer an explicit copy constructor or static factory:

final class Vector2 {
    final double x;
    final double y;

    Vector2(double x, double y) { this.x = x; this.y = y; }

    Vector2(Vector2 source) {                  // copy constructor
        this(source.x, source.y);
    }

    static Vector2 copyOf(Vector2 source) {    // or a static factory
        return new Vector2(source.x, source.y);
    }
}

var a = new Vector2(1, 2);
var b = new Vector2(a);        // clear, type-safe, respects final fields

See Also