Master Java inheritance using extends and super, implement runtime polymorphism through method overriding, and apply the Liskov Substitution Principle to build extensible class hierarchies.
Inheritance is the mechanism by which one class acquires the state and behavior of another. It enables code reuse — common logic lives in the parent class and is automatically available to all subclasses. Polymorphism is the companion mechanism: a variable of the parent type can hold any subtype, and method calls are routed to the correct implementation at runtime.
Together, inheritance and polymorphism power the open/closed principle: systems are open for extension (add new subclasses) but closed for modification (existing code that uses the parent type works without changes). This is why virtually every Java framework — Spring, Hibernate, JUnit — uses abstract base classes and interfaces extensively. When you understand polymorphism, you understand why framework code you wrote works without modifying the framework itself.
1// Parent (base/super) class
2public class Animal {
3 protected String name; // protected: accessible in subclasses
4 protected int age;
5
6 public Animal(String name, int age) {
7 this.name = name;
8 this.age = age;
9 }
10
11 public String getName() { return name; }
12 public int getAge() { return age; }
13
14 public String speak() {
15 return name + " makes a sound";
16 }
17
18 @Override
19 public String toString() {
20 return String.format("%s(%s, age %d)",
21 getClass().getSimpleName(), name, age);
22 }
23}
24
25// Child (derived/sub) class
26public class Dog extends Animal {
27 private String breed;
28
29 public Dog(String name, int age, String breed) {
30 super(name, age); // MUST call parent constructor first
31 this.breed = breed;
32 }
33
34 public String getBreed() { return breed; }
35
36 @Override // overrides parent method — runtime dispatch
37 public String speak() {
38 return name + " says: Woof!"; // accesses protected 'name'
39 }
40
41 // Subclass-specific method
42 public String fetch() {
43 return name + " fetches the ball!";
44 }
45}
46
47public class Cat extends Animal {
48 private boolean isIndoor;
49
50 public Cat(String name, int age, boolean isIndoor) {
51 super(name, age);
52 this.isIndoor = isIndoor;
53 }
54
55 @Override
56 public String speak() {
57 return name + " says: Meow!";
58 }
59}
60
61// Using the hierarchy
62Animal a = new Animal("Generic", 3);
63Dog d = new Dog("Rex", 2, "Labrador");
64Cat c = new Cat("Whiskers", 4, true);
65
66System.out.println(a.speak()); // Generic makes a sound
67System.out.println(d.speak()); // Rex says: Woof!
68System.out.println(c.speak()); // Whiskers says: Meow!1// Polymorphism: parent type reference, subtype object
2Animal[] animals = {
3 new Dog("Rex", 2, "Labrador"),
4 new Cat("Whiskers", 4, true),
5 new Dog("Buddy", 1, "Poodle"),
6 new Animal("Zoo Animal", 5)
7};
8
9// Dynamic dispatch: JVM calls the ACTUAL object's method at runtime
10for (Animal animal : animals) {
11 System.out.println(animal.speak()); // correct version called for each
12}
13// Rex says: Woof!
14// Whiskers says: Meow!
15// Buddy says: Woof!
16// Zoo Animal makes a sound
17
18// This is the power of polymorphism:
19// Adding a new subclass (Parrot) requires NO changes to this loop
20
21// Calling parent method from override using super
22public class GoldenRetriever extends Dog {
23 public GoldenRetriever(String name, int age) {
24 super(name, age, "Golden Retriever");
25 }
26
27 @Override
28 public String speak() {
29 return super.speak() + " (enthusiastically!)"; // extend parent behavior
30 }
31}
32
33Dog g = new GoldenRetriever("Max", 3);
34System.out.println(g.speak()); // Max says: Woof! (enthusiastically!)
35
36// Calling parent method in constructor chain
37public class ElectricCar extends Car {
38 private int batteryKwh;
39
40 public ElectricCar(String model, int year, int batteryKwh) {
41 super(model, year); // must be first statement
42 this.batteryKwh = batteryKwh;
43 }
44
45 @Override
46 public String toString() {
47 return super.toString() + " [EV, " + batteryKwh + " kWh]";
48 }
49}1// Traditional instanceof + cast
2Animal a = new Dog("Rex", 2, "Labrador");
3
4if (a instanceof Dog) {
5 Dog dog = (Dog) a; // explicit downcast
6 System.out.println(dog.fetch());
7}
8
9// Java 16+ pattern matching: combines instanceof check and cast
10if (a instanceof Dog dog) { // 'dog' is in scope only inside this if
11 System.out.println(dog.fetch()); // no cast needed!
12}
13
14// Pattern matching in switch (Java 21)
15void processAnimal(Animal animal) {
16 String message = switch (animal) {
17 case Dog d when d.getBreed().equals("Labrador") ->
18 d.getName() + " is a friendly Labrador";
19 case Dog d -> d.getName() + " is a " + d.getBreed();
20 case Cat c -> c.getName() + " is a cat";
21 default -> animal.getName() + " is an unknown animal";
22 };
23 System.out.println(message);
24}
25
26// Polymorphic method: works with any Animal subtype
27public class AnimalShelter {
28 private List<Animal> animals = new ArrayList<>();
29
30 public void admit(Animal animal) {
31 animals.add(animal);
32 System.out.println("Admitted: " + animal);
33 }
34
35 // Works for ALL animal types without modification
36 public void makeAllAnimalsSpeak() {
37 animals.forEach(a -> System.out.println(a.speak()));
38 }
39
40 public long countByType(Class<?> type) {
41 return animals.stream()
42 .filter(type::isInstance)
43 .count();
44 }
45}
46
47AnimalShelter shelter = new AnimalShelter();
48shelter.admit(new Dog("Rex", 2, "Lab"));
49shelter.admit(new Cat("Luna", 3, false));
50shelter.admit(new Dog("Buddy", 1, "Poodle"));
51shelter.makeAllAnimalsSpeak();
52System.out.println("Dogs: " + shelter.countByType(Dog.class)); // 21// final class: cannot be subclassed
2// String, Integer, and all primitive wrappers are final
3public final class ImmutableMoney {
4 private final long cents;
5 private final String currency;
6
7 public ImmutableMoney(long cents, String currency) {
8 this.cents = cents;
9 this.currency = currency;
10 }
11
12 public ImmutableMoney add(ImmutableMoney other) {
13 if (!this.currency.equals(other.currency))
14 throw new IllegalArgumentException("Currency mismatch");
15 return new ImmutableMoney(this.cents + other.cents, this.currency);
16 }
17
18 @Override
19 public String toString() {
20 return String.format("%s %.2f", currency, cents / 100.0);
21 }
22}
23
24// final method: cannot be overridden in subclasses
25public class Template {
26 // Template method pattern: final skeleton, abstract hooks
27 public final void execute() { // final: subclasses cannot override the flow
28 validateInput();
29 doProcess();
30 cleanup();
31 }
32
33 protected void validateInput() { /* default: no-op */ }
34 protected abstract void doProcess();
35 protected void cleanup() { /* default: no-op */ }
36}
37
38// Liskov Substitution Principle (LSP):
39// Subclasses must be substitutable for their base type
40// Violation example — DO NOT DO THIS:
41public class ReadOnlyList extends ArrayList<String> {
42 @Override
43 public boolean add(String item) {
44 throw new UnsupportedOperationException(); // LSP VIOLATION!
45 }
46}
47// Code expecting ArrayList<String> would break when given ReadOnlyList
48
49// Correct approach: use composition, not inheritance
50public class ReadOnlyListView {
51 private final List<String> delegate;
52 public ReadOnlyListView(List<String> list) {
53 this.delegate = Collections.unmodifiableList(list);
54 }
55 public String get(int index) { return delegate.get(index); }
56 public int size() { return delegate.size(); }
57}1// Every Java class implicitly extends Object
2// Object methods available on EVERY Java object:
3
4public class Demonstration {
5 public static void main(String[] args) {
6 Object obj = new Object();
7
8 // toString(): returns class@hashcode by default
9 System.out.println(obj.toString());
10 // e.g., java.lang.Object@7ef88735
11
12 // equals(): identity equality by default (use == behavior)
13 Object a = new Object();
14 Object b = new Object();
15 System.out.println(a.equals(b)); // false — different objects
16 System.out.println(a.equals(a)); // true — same object
17
18 // hashCode(): based on memory address by default
19 System.out.println(a.hashCode()); // some integer
20
21 // getClass(): runtime class info
22 String s = "hello";
23 System.out.println(s.getClass().getSimpleName()); // String
24 System.out.println(s.getClass().getName()); // java.lang.String
25
26 // instanceof chain for safe type checking
27 Object item = "hello";
28 if (item instanceof String str) { // pattern matching
29 System.out.println(str.toUpperCase()); // HELLO
30 }
31
32 // Useful Object methods: wait(), notify(), notifyAll() (for threading)
33 // clone() — rarely used, prefer copy constructors
34 // finalize() — deprecated, do not use
35 }
36}
37
38// Inheritance summary:
39// - 'extends' establishes IS-A relationship
40// - super() must be first call in constructor
41// - @Override catches overriding mistakes at compile time
42// - Prefer composition over inheritance when IS-A is unclear
43// - Abstract classes enforce that subclasses implement required methodsA complete geometric shape hierarchy demonstrating inheritance, method overriding, polymorphic dispatch, instanceof pattern matching, and the Liskov Substitution Principle.
1import java.util.ArrayList;
2import java.util.List;
3import java.util.OptionalDouble;
4
5/**
6 * Shape hierarchy demonstrating inheritance and polymorphism.
7 */
8public abstract class Shape {
9 protected final String color;
10
11 public Shape(String color) {
12 this.color = (color != null) ? color : "white";
13 }
14
15 // Abstract: subclasses MUST implement
16 public abstract double area();
17 public abstract double perimeter();
18
19 // Concrete: inherited by all subclasses
20 public String getColor() { return color; }
21
22 public boolean isLargerThan(Shape other) {
23 return this.area() > other.area();
24 }
25
26 @Override
27 public String toString() {
28 return String.format("%s{color=%s, area=%.2f, perimeter=%.2f}",
29 getClass().getSimpleName(), color, area(), perimeter());
30 }
31}
32
33class Circle extends Shape {
34 private final double radius;
35
36 public Circle(double radius, String color) {
37 super(color);
38 if (radius <= 0) throw new IllegalArgumentException("Radius must be > 0");
39 this.radius = radius;
40 }
41
42 @Override public double area() { return Math.PI * radius * radius; }
43 @Override public double perimeter() { return 2 * Math.PI * radius; }
44 public double getRadius() { return radius; }
45}
46
47class Rectangle extends Shape {
48 protected final double width;
49 protected final double height;
50
51 public Rectangle(double width, double height, String color) {
52 super(color);
53 if (width <= 0 || height <= 0)
54 throw new IllegalArgumentException("Dimensions must be > 0");
55 this.width = width;
56 this.height = height;
57 }
58
59 @Override public double area() { return width * height; }
60 @Override public double perimeter() { return 2 * (width + height); }
61}
62
63class Square extends Rectangle {
64 public Square(double side, String color) {
65 super(side, side, color); // delegates to Rectangle
66 }
67
68 public double getSide() { return width; }
69
70 // Square IS-A Rectangle — all Rectangle operations are valid
71 // area() and perimeter() work correctly via Rectangle's implementations
72}
73
74class Triangle extends Shape {
75 private final double a, b, c; // sides
76
77 public Triangle(double a, double b, double c, String color) {
78 super(color);
79 if (a + b <= c || b + c <= a || a + c <= b)
80 throw new IllegalArgumentException("Invalid triangle sides");
81 this.a = a; this.b = b; this.c = c;
82 }
83
84 @Override
85 public double area() {
86 double s = (a + b + c) / 2; // semi-perimeter
87 return Math.sqrt(s * (s - a) * (s - b) * (s - c)); // Heron's formula
88 }
89
90 @Override
91 public double perimeter() { return a + b + c; }
92}
93
94class ShapeAnalyzer {
95 public static void analyzeShapes(List<Shape> shapes) {
96 System.out.println("=== Shape Analysis ===");
97
98 // Polymorphic iteration — no type checking needed
99 for (Shape shape : shapes) {
100 System.out.println(shape);
101 }
102
103 // Statistics using polymorphic area()
104 OptionalDouble maxArea = shapes.stream()
105 .mapToDouble(Shape::area)
106 .max();
107
108 double totalArea = shapes.stream()
109 .mapToDouble(Shape::area)
110 .sum();
111
112 System.out.printf("%nTotal area: %.2f%n", totalArea);
113 System.out.printf("Largest area: %.2f%n", maxArea.orElse(0));
114
115 // Type-specific operations with instanceof pattern matching
116 System.out.println("\n--- Type-specific details ---");
117 for (Shape shape : shapes) {
118 if (shape instanceof Circle c) {
119 System.out.printf("Circle radius: %.2f%n", c.getRadius());
120 } else if (shape instanceof Square sq) {
121 System.out.printf("Square side: %.2f%n", sq.getSide());
122 } else if (shape instanceof Rectangle r) {
123 System.out.printf("Rectangle %s x %s%n", r.width, r.height);
124 }
125 }
126 }
127
128 public static void main(String[] args) {
129 List<Shape> shapes = new ArrayList<>();
130 shapes.add(new Circle(5.0, "red"));
131 shapes.add(new Rectangle(4.0, 6.0, "blue"));
132 shapes.add(new Square(3.0, "green"));
133 shapes.add(new Triangle(3.0, 4.0, 5.0, "yellow"));
134
135 analyzeShapes(shapes);
136
137 // Polymorphic comparison
138 Shape big = new Circle(10.0, "red");
139 Shape small = new Square(2.0, "blue");
140 System.out.printf("%nbig.isLargerThan(small): %b%n", big.isLargerThan(small));
141 }
142}This hierarchy shows inheritance in action: Shape is the abstract base with area() and perimeter() as abstract methods, Circle, Rectangle, and Triangle each override them with appropriate formulas, Square extends Rectangle (a genuine IS-A relationship — every square is a rectangle), and ShapeAnalyzer.analyzeShapes() works with List<Shape> and calls area() polymorphically without knowing the concrete type. The instanceof pattern matching section shows how to access subtype-specific methods only when needed. The stream-based statistics demonstrate how polymorphism enables writing generic algorithms over heterogeneous collections.
The next lesson covers abstract classes and interfaces — the mechanisms for defining contracts and supporting true multiple-type inheritance. You will learn when to choose an abstract class versus an interface, how default methods work in interfaces (Java 8+), and how to design extensible APIs that don't require changing existing code when new types are added.
Your progress is saved in this browser, on this device only.