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:
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
-
Classes and Objects — constructors, fields, and access control that inheritance builds on.
-
Interfaces — inheritance of type without implementation, and
defaultmethods. -
Records and Sealed Classes — transparent carriers and restricted hierarchies.
-
Methods and Parameters — overloading and the
Objectshelpers used inequals/hashCode.