Polymorphism lets one piece of code work with many types, and you meet inheritance in every framework and library.
A class can extend one other class and inherit its fields and methods: class Dog extends Animal. The subclass can add members and override inherited methods to change behaviour. Every class ultimately extends java.lang.Object, which is where toString, equals and hashCode come from.
Mark overrides with @Override. The compiler then checks that you really override something; without it, a typo like tostring() silently creates a new method.
Polymorphism is the payoff: a variable of type Animal can refer to a Dog or a Cat, and calling animal.sound() runs the override of the object's actual class, decided at runtime (dynamic dispatch). Code written against Animal works for subclasses that didn't exist when it was written.
Use super.method() to call the parent's version from an override, and super(...) as the first statement of a constructor to run the parent's constructor. If you don't, the compiler inserts super() — which fails if the parent has no no-arg constructor.
Modifiers that limit inheritance:
finalclass — can't be extended (Stringis final).finalmethod — can't be overridden.abstractclass — can't be instantiated and may declareabstractmethods without a body that subclasses must implement.
Inheritance creates tight coupling: subclasses depend on the parent's implementation details. The classic advice is to prefer composition over inheritance — hold a reference to a helper object instead of extending it — and use inheritance for genuine "is-a" relationships. For shared contracts, interfaces (next lesson) are usually the better tool.
Example
import java.util.List;
public class Shapes {
static abstract class Shape {
abstract double area(); // subclasses must implement
@Override public String toString() {
return getClass().getSimpleName() + " area=" + String.format("%.1f", area());
}
}
static class Circle extends Shape {
final double r;
Circle(double r) { this.r = r; }
@Override double area() { return Math.PI * r * r; }
}
static class Square extends Shape {
final double side;
Square(double side) { this.side = side; }
@Override double area() { return side * side; }
}
public static void main(String[] args) {
List<Shape> shapes = List.of(new Circle(1), new Square(2));
for (Shape s : shapes) System.out.println(s); // Circle area=3.1, then Square area=4.0
}
}Remember
extendsinherits members; overriding replaces behaviour for the subclass- Calls dispatch on the object's runtime class, not the variable's type
- Always write
@Override; prefer composition when "is-a" doesn't truly hold
Watch out for
- Omitting
@Overrideand misspelling the method name, so the parent's version still runs.