Inheritance and Polymorphism
Inheritance
Section titled “Inheritance”Inheritance allows a class (subclass) to extend another class (superclass), acquiring its fields and Methods. The extends keyword establishes an “is-a” relationship. Java supports single class Inheritance — a class can extend exactly one superclass — but can implement multiple interfaces.
The extends Keyword
Section titled “The extends Keyword”public class Animal { private String name; private int age;
public Animal(String name, int age) { this.name = name; this.age = age; }
public void eat() { System.out.println(name + " is eating"); }
public String getName() { return name; } public int getAge() { return age; }}
public class Dog extends Animal { private String breed;
public Dog(String name, int age, String breed) { super(name, age); // must be first statement this.breed = breed; }
@Override public void eat() { System.out.println(getName() + " the " + breed + " is eating kibble"); }}Method Overriding with @Override
Section titled “Method Overriding with @Override”A subclass can override a non-final, non-static method of its superclass. The @Override annotation Tells the compiler to verify that you are actually overriding a superclass method. If you misspell The method name or get the signature wrong, the compiler will report an error instead of silently Creating an overloaded method.
public class Dog extends Animal { @Override public void eat() { System.out.println(getName() + " is chewing"); }
// Compiler error if Animal.eat() does not exist // @Override // public void eet() { }}Rules for overriding:
- The method must have the same name, return type, and parameter list.
- The access level cannot be more restrictive (can widen, not narrow).
- Cannot override
static``finalOrprivatemethods (static methods are hidden, not overridden). - Cannot throw checked exceptions that are broader than the superclass method”s exceptions (can narrow, not widen).
The super Keyword
Section titled “The super Keyword”super has two uses: calling a superclass constructor and accessing a superclass member (field or Method).
public class Labrador extends Dog { private boolean isGuideDog;
public Labrador(String name, int age, String color, boolean isGuideDog) { super(name, age, "Labrador " + color); // superclass constructor this.isGuideDog = isGuideDog; }
@Override public void eat() { super.eat(); // calls Dog.eat() System.out.println(" (especially guide dog food: " + isGuideDog + ")"); }}Constructors in Inheritance
Section titled “Constructors in Inheritance”The first statement of every constructor is either an explicit super(...) call or an implicit super() (no-arg constructor). If the superclass has no no-arg constructor, the subclass must Explicitly call a superclass constructor with arguments.
public class Base { private final int value;
public Base(int value) { this.value = value; // No no-arg constructor exists }}
public class Derived extends Base { public Derived() { super(42); // REQUIRED — Base has no no-arg constructor }
public Derived(int value) { super(value); // explicitly calling parameterized constructor }}Constructor execution order: superclass constructor runs first, then subclass fields are Initialized, then subclass constructor body executes. This ensures that the superclass part of the Object is fully constructed before the subclass adds its own state.
public class A { public A() { System.out.println("A constructor"); }}
public class B extends A { private static final Logger log = Logger.getLogger(B.class.getName());
public B() { super(); // implicit System.out.println("B constructor"); }}
new B();// Output:// A constructor// B constructorAccess Modifiers
Section titled “Access Modifiers”| Modifier | Class | Package | Subclass | World |
|---|---|---|---|---|
public | Yes | Yes | Yes | Yes |
protected | Yes | Yes | Yes | No |
| (package-private) | Yes | Yes | No | No |
private | Yes | No | No | No |
Package-Private (Default Access)
Section titled “Package-Private (Default Access)”If you specify no access modifier, the member is accessible within the same package only. This is Often the right choice for implementation details that should be shared among classes in the same Package but hidden from external code.
class PackagePrivateClass { // accessible within same package only int packageField; // accessible within same package only}protected vs Package-Private
Section titled “protected vs Package-Private”protected grants access to subclasses regardless of package. This is a wider scope than many Developers expect. Use protected only when subclasses genuinely need direct access to the member.
public class Base { protected int counter; // visible to subclasses AND same-package classes
// Prefer protected methods over protected fields protected int getCounter() { return counter; }}Polymorphism
Section titled “Polymorphism”Compile-Time vs Runtime Polymorphism
Section titled “Compile-Time vs Runtime Polymorphism”Compile-time polymorphism (method overloading) — the compiler resolves which method to call Based on the static types of the arguments at compile time.
Runtime polymorphism (method overriding) — the JVM resolves which method to call based on the Actual type of the object at runtime. This is the polymorphism that matters for the “is-a” Relationship.
Animal animal = new Dog("Rex", 3, "Shepherd");animal.eat(); // Calls Dog.eat() — runtime dispatch
// The variable type is Animal, but the actual object is Dog// The JVM calls Dog's overridden eat() methodVirtual Method Dispatch
Section titled “Virtual Method Dispatch”In Java, all non-static, non-final instance methods are virtual by default. When you call a Method on an object reference, the JVM looks up the method in the object’s actual class (not the Declared type of the reference). This is called dynamic dispatch or virtual method Invocation.
The JVM uses a vtable (virtual method table) to implement this efficiently. Each class has a vtable Containing pointers to its virtual methods. When a subclass overrides a method, its vtable entry is Replaced with a pointer to the overriding method.
public class Shape { public double area() { return 0; }}
public class Circle extends Shape { private final double radius; public Circle(double radius) { this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; }}
public class Rectangle extends Shape { private final double width, height; public Rectangle(double w, double h) { width = w; height = h; } @Override public double area() { return width * height; }}
// Runtime dispatch in actionShape s = Math.random() > 0.5 ? new Circle(5) : new Rectangle(3, 4);s.area(); // The JVM calls the correct area() based on the actual object typeDynamic Binding and final
Section titled “Dynamic Binding and final”The final keyword on a method prevents overriding and allows the JIT compiler to devirtualize the Call — it can inline the method at the call site because it knows no subclass will override it. This Can improve performance in hot paths.
public class Point { private final double x, y;
public Point(double x, double y) { this.x = x; this.y = y; }
// JIT can inline this — no subclass can override it public final double distanceTo(Point other) { double dx = this.x - other.x; double dy = this.y - other.y; return Math.sqrt(dx * dx + dy * dy); }}Abstract Classes
Section titled “Abstract Classes”An abstract class is a class declared with the abstract keyword. It may contain abstract methods (declared without a body) and concrete methods. You cannot instantiate an abstract class directly.
public abstract class Shape { private final String color;
protected Shape(String color) { this.color = color; }
// Abstract method — subclasses must implement public abstract double area(); public abstract double perimeter();
// Concrete method — subclasses inherit public String getColor() { return color; }
// Template method pattern public void describe() { System.out.printf("Shape: %s, Area: %.2f, Perimeter: %.2f%n", color, area(), perimeter()); }}
public class Triangle extends Shape { private final double a, b, c;
public Triangle(double a, double b, double c, String color) { super(color); this.a = a; this.b = b; this.c = c; }
@Override public double area() { double s = (a + b + c) / 2; return Math.sqrt(s * (s - a) * (s - b) * (s - c)); }
@Override public double perimeter() { return a + b + c; }}When to Use Abstract Classes
Section titled “When to Use Abstract Classes”Use abstract classes when:
- You want to share code among closely related classes (common fields, utility methods).
- You need to declare fields that subclasses will use.
- You want to use the template method pattern (base class defines the algorithm skeleton, subclasses fill in steps).
- You need to control access to state (fields can be
private).
Abstract classes provide “is-a” semantics with shared implementation. Interfaces provide “can-do” Semantics with no shared state (prior to Java 8).
Interfaces
Section titled “Interfaces”An interface defines a contract that implementing classes must fulfill. Since Java 8, interfaces can Have default methods, static methods, and (since Java 9) private methods.
Interface Evolution
Section titled “Interface Evolution”public interface Drawable { // Abstract method — implementing classes must provide an implementation void draw();
// Default method — provides a default implementation (Java 8+) default void drawWithBorder(int borderWidth) { System.out.println("Drawing border of width " + borderWidth); draw(); }
// Static method — belongs to the interface, not implementing instances (Java 8+) static void drawAll(Drawable[] drawables) { for (Drawable d : drawables) { d.draw(); } }
// Private method — helper for default methods (Java 9+) private void logDraw() { System.out.println("Drawing performed"); }}Functional Interfaces
Section titled “Functional Interfaces”A functional interface has exactly one abstract method. It can be used as the target of a lambda Expression. The @FunctionalInterface annotation is optional but recommended — the compiler will Verify the constraint.
@FunctionalInterfacepublic interface Transformer<T, R> { R transform(T input);}
// Lambda usageTransformer<String, Integer> lengthExtractor = String::length;int len = lengthExtractor.transform("hello"); // 5Built-in functional interfaces in java.util.function:
| Interface | Abstract Method | Signature |
|---|---|---|
Predicate<T> | boolean test(T) | T -> boolean |
Function<T,R> | R apply(T) | T -> R |
Consumer<T> | void accept(T) | T -> void |
Supplier<T> | T get() | () -> T |
UnaryOperator<T> | T apply(T) | T -> T |
BinaryOperator<T> | T apply(T, T) | (T,T) -> T |
BiFunction<T,U,R> | R apply(T, U) | (T,U) -> R |
Multiple Inheritance of Interfaces
Section titled “Multiple Inheritance of Interfaces”A class can implement multiple interfaces. If two interfaces declare the same default method, the Implementing class must resolve the conflict:
interface Walkable { default void move() { System.out.println("Walking"); }}
interface Swimmable { default void move() { System.out.println("Swimming"); }}
// Must override to resolve conflictclass Duck implements Walkable, Swimmable { @Override public void move() { Walkable.super.move(); // explicitly choose one }}If a class extends a superclass and implements an interface that both define a method with the same Signature, the superclass method wins (class wins over interface). This is called “class-first” Rule.
Composition Over Inheritance
Section titled “Composition Over Inheritance”Inheritance creates tight coupling between superclass and subclass. Changes to the superclass can Break subclasses in unexpected ways. Composition — building complex objects from simpler ones — Provides flexibility and loose coupling.
// INHERITANCE — tight couplingpublic class FlyingCar extends Car { @Override public void drive() { super.drive(); fly(); } private void fly() { /* ... */ }}
// COMPOSITION — loose coupling, flexiblepublic class FlyingVehicle { private final GroundNavigation groundNav = new GroundNavigation(); private final AirNavigation airNav = new AirNavigation();
public void drive() { groundNav.navigate(); } public void fly() { airNav.navigate(); }}Favor composition when:
- The “is-a” relationship does not hold cleanly.
- You need behavior from multiple sources (Java’s single inheritance limits this).
- Subclasses would override most superclass methods anyway.
- You need runtime flexibility (swap implementations).
Use inheritance when:
- The “is-a” relationship is clear and stable.
- You genuinely want to share implementation code.
- The superclass is part of a framework/library that expects extension (e.g.,
HttpServlet). - You need polymorphic behavior via virtual dispatch.
Liskov Substitution Principle
Section titled “Liskov Substitution Principle”The Liskov Substitution Principle (LSP) states that if S is a subtype of T, then objects of type T May be replaced with objects of type S without altering any of the desirable properties of the Program. In practical terms: a subclass must be usable anywhere the superclass is expected.
// LSP VIOLATIONpublic class Rectangle { private int width, height;
public void setWidth(int w) { this.width = w; } public void setHeight(int h) { this.height = h; } public int getArea() { return width * height; }}
// A square IS a rectangle geometrically, but NOT behaviorallypublic class Square extends Rectangle { @Override public void setWidth(int w) { super.setWidth(w); super.setHeight(w); // breaks the LSP contract }
@Override public void setHeight(int h) { super.setHeight(h); super.setWidth(h); }}
// Code that works with Rectangle breaks with Squarevoid resize(Rectangle r, int w, int h) { r.setWidth(w); r.setHeight(h); assert r.getArea() == w * h; // FAILS for Square!}| Principle | Guideline |
|---|---|
| S — Single Responsibility | A class should have only one reason to change. |
| O — Open/Closed | Open for extension, closed for modification. |
| L — Liskov Substitution | Subtypes must be substitutable for their base types. |
| I — Interface Segregation | Prefer many specific interfaces over one general-purpose interface. |
| D — Dependency Inversion | Depend on abstractions, not concretions. |
Single Responsibility Example
Section titled “Single Responsibility Example”// VIOLATION — class does too many thingspublic class UserService { public void saveUser(User user) { /* DB access */ } public void sendEmail(User user, String msg) { /* email sending */ } public void validateUser(User user) { /* validation */ }}
// CORRECT — separate responsibilitiespublic class UserRepository { public void save(User user) { /* DB access */ }}public class EmailService { public void send(String to, String message) { /* email */ }}public class UserValidator { public void validate(User user) { /* validation */ }}Open/Closed Example
Section titled “Open/Closed Example”// Open for extension (add new shapes) without modifying existing codepublic interface AreaCalculator { double calculate(Shape shape);}
public class CircleAreaCalculator implements AreaCalculator { @Override public double calculate(Shape shape) { Circle c = (Circle) shape; return Math.PI * c.radius() * c.radius(); }}
// Add a new shape without changing existing calculatorspublic class RectangleAreaCalculator implements AreaCalculator { @Override public double calculate(Shape shape) { Rectangle r = (Rectangle) shape; return r.width() * r.height(); }}Dependency Inversion Example
Section titled “Dependency Inversion Example”// HIGH-LEVEL module depends on ABSTRACTION, not on LOW-LEVEL detailpublic interface DataSource { String fetchData(String query);}
public class DatabaseDataSource implements DataSource { @Override public String fetchData(String query) { /* SQL */ }}
public class FileDataSource implements DataSource { @Override public String fetchData(String query) { /* file read */ }}
public class ReportService { private final DataSource dataSource;
// Inject the dependency — don't create it here public ReportService(DataSource dataSource) { this.dataSource = dataSource; }}Object Casting
Section titled “Object Casting”Downcasting and instanceof
Section titled “Downcasting and instanceof”Animal animal = new Dog("Rex", 3, "Shepherd");
// Upcasting — always safe, implicitAnimal a = animal;
// Downcasting — may fail, requires explicit castDog dog = (Dog) a; // OK — a actually refers to a Dog// Cat cat = (Cat) a; // ClassCastException at runtime
// Safe downcasting with instanceofif (a instanceof Dog d) { d.bark(); // pattern variable d is in scope (Java 16+)}Pattern Matching with switch (JDK 21)
Section titled “Pattern Matching with switch (JDK 21)”Java 21 finalized pattern matching for switch, allowing you to match on types directly:
public String describe(Object obj) { return switch (obj) { case Integer i -> "Integer: " + i; case String s when s.length() > 10 -> "Long string: " + s.substring(0, 10) + "..."; case String s -> "String: " + s; case int[] arr -> "Array of " + arr.length + " ints"; case null -> "null value"; default -> "Unknown: " + obj.getClass().getSimpleName(); };}Rules for pattern matching in switch:
- Case labels are matched top-to-bottom. More specific patterns must come before more general ones.
- The
nullcase must come first (or be handled with a nullable patterncase String swhich matches non-null strings). - A
defaultcase is required if the switch expression does not cover all possible values. - Guards (
whenclauses) refine pattern matching with boolean conditions.
// Sealed hierarchy with exhaustive pattern matchingpublic sealed interface Shape permits Circle, Rectangle, Triangle {}public record Circle(double radius) implements Shape {}public record Rectangle(double width, double height) implements Shape {}public record Triangle(double a, double b, double c) implements Shape {}
public double area(Shape shape) { return switch (shape) { case Circle c -> Math.PI * c.radius() * c.radius(); case Rectangle r -> r.width() * r.height(); case Triangle t -> { double s = (t.a() + t.b() + t.c()) / 2; yield Math.sqrt(s * (s - t.a()) * (s - t.b()) * (s - t.c())); } // No default needed — sealed interface ensures exhaustive matching };}Sealed Classes for Restricted Hierarchies
Section titled “Sealed Classes for Restricted Hierarchies”Sealed classes (JDK 17) restrict which classes can extend or implement them. This enables the Compiler to verify exhaustive pattern matching.
public sealed class Expr permits Literal, Add, Multiply, Negate { // ...}
public final class Literal extends Expr { private final int value; public Literal(int value) { this.value = value; } public int value() { return value; }}
public final class Add extends Expr { private final Expr left, right; public Add(Expr left, Expr right) { this.left = left; this.right = right; } public Expr left() { return left; } public Expr right() { return right; }}
public non-sealed class Multiply extends Expr { // non-sealed allows further extension}Permitted subclass rules:
- The permitted subclasses must be in the same module (if the sealed class is in a named module) or the same package (if in the unnamed module).
- Permitted subclasses must use one of:
final``sealedOrnon-sealed. - The sealed class and its permitted subclasses must co-compile (or be in the same compilation unit).
Record Patterns
Section titled “Record Patterns”Record patterns (JDK 21, JEP 440) destructure record instances directly in pattern matching:
record Point(double x, double y) {}record ColoredPoint(Point point, String color) {}record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}
// Destructure a recordstatic void printColorOfUpperLeftPoint(Rectangle r) { if (r instanceof Rectangle(ColoredPoint(Point(var x, var y), var color), var other)) { System.out.println("Upper-left corner: (" + x + "," + y + "), color: " + color); }}
// In switchstatic void printGeometry(Object obj) { switch (obj) { case Point(var x, var y) -> System.out.println("Point at (" + x + "," + y + ")"); case ColoredPoint(Point(var x, var y), var color) -> System.out.println("Colored point at (" + x + "," + y + "), color: " + color); case Rectangle(ColoredPoint(Point(var x1, var y1), var c1), ColoredPoint(Point(var x2, var y2), var c2)) -> System.out.printf("Rectangle from (%.1f,%.1f) to (%.1f,%.1f)%n", x1, y1, x2, y2); default -> System.out.println("Not a geometric shape"); }}Nested Record Patterns
Section titled “Nested Record Patterns”Record patterns can be nested arbitrarily deep, allowing you to match the internal structure of Complex data:
// Deep nestingif (r instanceof Rectangle( ColoredPoint(Point(var x1, var y1), var c1), ColoredPoint(Point(var x2, var y2), var c2))) { // All variables are extracted and in scope}Family resemblance: Inheritance is like a family tree — child classes inherit traits from parents but can also have their own unique characteristics. Polymorphism lets different shapes respond to the same command in their own way.
Why it matters: Inheritance promotes code reuse, and polymorphism enables flexible, extensible designs. Together they power the open-closed principle.
The key insight: Program to an interface, not an implementation — this makes your code flexible and easy to extend.
Common Pitfalls
Section titled “Common Pitfalls”Forgetting super() in Constructor
Section titled “Forgetting super() in Constructor”public class Derived extends Base { public Derived(int value) { // BUG — implicit super() calls Base(), but Base(int) is the only constructor // Compiler error: constructor Base() not found }}Overloading vs Overriding Confusion
Section titled “Overloading vs Overriding Confusion”public class Base { public void process(List<String> items) { /* ... */ }}
public class Derived extends Base { // BUG — this is overloading, NOT overriding // Parameter type is different (ArrayList vs List) public void process(ArrayList<String> items) { /* ... */ }
// CORRECT overriding @Override public void process(List<String> items) { /* ... */ }}Calling Overridable Methods in Constructor
Section titled “Calling Overridable Methods in Constructor”public class Base { public Base() { initialize(); // DANGEROUS — calls overridden method before subclass is constructed }
protected void initialize() { // subclass may override this }}
public class Derived extends Base { private final List<String> data = new ArrayList<>();
@Override protected void initialize() { data.add("initialized"); // NullPointerException — data not yet initialized }}
new Derived(); // throws NullPointerException