Free Handbook · Every example compiled & verified

Inheritance & Interfaces

Java inheritance and interfaces: extends, super, @Override, dynamic dispatch, abstract classes, default methods, sealed types and composition.

0 / 142 lessons🔥 0 day streak
ShareXLinkedIn

Module 06 · what you'll be able to do

  • Build a subclass with extends, call the parent constructor with super(...) and override methods safely with @Override
  • Predict which method runs by tracing dynamic dispatch: the object's runtime class decides, not the variable's type
  • Choose between an abstract class and an interface, and use default and static interface methods
  • Model a closed set of cases with a sealed interface and records, then handle them with an exhaustive pattern-matching switch
  • Recognise when inheritance is the wrong tool and refactor to composition
01

extends and super: building on a class

Inheritance lets one class start from another. class Dog extends Animal means a Dog is an Animal: it gets every public and protected field and method of Animal for free and can add its own. Animal is the superclass (parent), Dog the subclass (child). Java allows exactly one superclass per class; a class that names none extends Object.

javaMain.java
public class Main {
    public static void main(String[] args) {
        Dog rex = new Dog("Rex");
        rex.eat();        // inherited from Animal
        rex.bark();       // defined in Dog
        System.out.println(rex.name + " is an Animal? " + (rex instanceof Animal));
    }
}

class Animal {
    protected String name;

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

    void eat() {
        System.out.println(name + " is eating");
    }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);      // run Animal's constructor first
    }

    void bark() {
        System.out.println(name + " says woof");
    }
}
Outputcompiled & run with real Java
Rex is eating
Rex says woof
Rex is an Animal? true
Your turn

Add a Cat subclass with a meow() method and call both eat() and meow() on it.

Constructors run parent-first

Constructors are not inherited. Every constructor must first run a constructor of its parent, written as super(...) on its first line. If you leave it out, the compiler silently inserts super() — the no-argument version. So an object is built from the top of the hierarchy down: Object, then the parent, then the child.

javaMain.java
public class Main {
    public static void main(String[] args) {
        new Puppy();
    }
}

class Animal {
    Animal() { System.out.println("1. Animal constructor"); }
}

class Dog extends Animal {
    Dog() { System.out.println("2. Dog constructor"); }       // implicit super()
}

class Puppy extends Dog {
    Puppy() { System.out.println("3. Puppy constructor"); }   // implicit super()
}
Outputcompiled & run with real Java
1. Animal constructor
2. Dog constructor
3. Puppy constructor

Even though main only asks for a Puppy, all three constructors run, parent first.

Error you will hit

constructor Animal in class Animal cannot be applied to given types

java
public class Main {
    public static void main(String[] args) {
        System.out.println(new Dog().name);
    }
}

class Animal {
    String name;
    Animal(String name) { this.name = name; }
}

class Dog extends Animal {
    Dog() {
        System.out.println("making a dog");
    }
}
Main.java:13: error: constructor Animal in class Animal cannot be applied to given types;
    Dog() {
          ^
  required: String
  found:    no arguments
  reason: actual and formal argument lists differ in length
1 error
Why the compiler said that

Dog() has no explicit super(...), so the compiler inserted super(). But Animal only has a constructor that takes a String — declaring any constructor removes the free no-argument one. There is nothing for super() to call.

The fix

Call the constructor that exists, as the very first statement: super("Rex"); — or pass a name into Dog's constructor and forward it.

java
public class Main {
    public static void main(String[] args) {
        System.out.println(new Dog("Rex").name);
    }
}

class Animal {
    String name;
    Animal(String name) { this.name = name; }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
        System.out.println("making a dog");
    }
}
What a subclass cannot see
private members of the parent exist inside the child object but the child's code cannot touch them — use the parent's public methods, or make the member protected if subclasses genuinely need it. Access modifiers are covered in Module 05 · Classes & Objects.
02

Method overriding and @Override

A subclass overrides a method by declaring one with the same name and the same parameter types. When that method is called on the subclass object, the subclass version runs. Inside it, super.method() still reaches the parent version — handy when you want to extend behaviour rather than replace it.

javaMain.java
public class Main {
    public static void main(String[] args) {
        Account basic = new Account(100);
        Account savings = new SavingsAccount(100);
        System.out.println(basic.describe());
        System.out.println(savings.describe());
    }
}

class Account {
    protected double balance;
    Account(double balance) { this.balance = balance; }

    String describe() {
        return "Account with " + balance;
    }
}

class SavingsAccount extends Account {
    SavingsAccount(double balance) { super(balance); }

    @Override
    String describe() {
        return super.describe() + " (earns 4% interest)";   // reuse the parent's text
    }
}
Outputcompiled & run with real Java
Account with 100.0
Account with 100.0 (earns 4% interest)

Why @Override is not optional in practice

@Override is an annotation that asks the compiler to check that this method really overrides something. Without it, a typo in the name or a wrong parameter type silently creates a brand-new method, and the parent version keeps running. With it, the typo becomes a compile error.

Error you will hit

method does not override or implement a method from a supertype

java
public class Main {
    public static void main(String[] args) {
        System.out.println(new Point(1, 2));
    }
}

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

    @Override
    public String toSting() {          // typo: toSting
        return "(" + x + ", " + y + ")";
    }
}
Main.java:11: error: method does not override or implement a method from a supertype
    @Override
    ^
1 error
Why the compiler said that

Object has toString(), not toSting(). Because the method is marked @Override, the compiler looked for a matching method in the parents, found none, and refused. Without the annotation this would compile and print something like Point@1b6d3586.

The fix

Fix the name so the signature matches exactly: public String toString().

java
public class Main {
    public static void main(String[] args) {
        System.out.println(new Point(1, 2));
    }
}

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

    @Override
    public String toString() {
        return "(" + x + ", " + y + ")";
    }
}
Rule for an overrideWhy
Same name and same parameter typesDifferent parameters make it an overload — a separate method
Return type the same or a subtype (covariant)Callers expecting the parent's type must still be satisfied
Access the same or wider (protected → public is fine, not the reverse)Code that could call the parent method must be able to call the child's
May not throw new or broader checked exceptionsCallers only handle what the parent declared
static, private and final methods cannot be overriddenStatic and private methods are not dispatched per object; final forbids it
Error you will hit

attempting to assign weaker access privileges

java
public class Main {
    public static void main(String[] args) {
        System.out.println(new Circle().name());
    }
}

class Shape {
    public String name() { return "shape"; }
}

class Circle extends Shape {
    @Override
    String name() { return "circle"; }    // package-private: narrower than public
}
Main.java:13: error: name() in Circle cannot override name() in Shape
    String name() { return "circle"; }    // package-private: narrower than public
           ^
  attempting to assign weaker access privileges; was public
1 error
Why the compiler said that

Anyone holding a Shape may call name() because it is public. If Circle could narrow it, a Shape variable pointing at a Circle would expose a method the Circle says is not public. Java forbids overrides from narrowing access.

The fix

Keep the access at least as wide as the parent's: public String name().

03

Polymorphism and dynamic dispatch

A variable of a parent type can hold any subclass object: Shape s = new Circle(2);. Two different types are now in play. The static type (Shape, the declared type) decides what you are allowed to call — checked by the compiler. The runtime type (Circle, the actual object) decides which version runs — chosen by the JVM at the moment of the call. That runtime choice is dynamic dispatch, and it is what makes polymorphism work: one loop, many behaviours.

javaMain.java
public class Main {
    public static void main(String[] args) {
        Shape[] shapes = { new Circle(1), new Square(2), new Shape() };
        double total = 0;
        for (Shape s : shapes) {
            System.out.printf("%-6s area %.2f%n", s.name(), s.area());
            total += s.area();
        }
        System.out.printf("total  area %.2f%n", total);
    }
}

class Shape {
    String name() { return "shape"; }
    double area() { return 0; }
}

class Circle extends Shape {
    double r;
    Circle(double r) { this.r = r; }
    @Override String name() { return "circle"; }
    @Override double area() { return Math.PI * r * r; }
}

class Square extends Shape {
    double side;
    Square(double side) { this.side = side; }
    @Override String name() { return "square"; }
    @Override double area() { return side * side; }
}
Outputcompiled & run with real Java
circle area 3.14
square area 4.00
shape  area 0.00
total  area 7.14
Your turn

Add a Rectangle with width and height, put one in the array, and notice that the loop needs no change at all.

VisualizeWhich area() runs? The object decidesStep 1 / 6
Shape a = new Circle(1);
Shape b = new Square(2);
double x = a.area();
double y = b.area();
Shape c = b;
double z = c.area();
Line 1

The static type of a is Shape; the object it points at is a Circle.

Variables now
aCircle(r=1)
All 6 steps as a table
StepLineWhat happenedVariables now
11The static type of a is Shape; the object it points at is a Circle.a = Circle(r=1)
22Same for b: declared Shape, actually a Square.b = Square(side=2)
33The compiler only checks that Shape has an area() method. At runtime the JVM looks at the object, sees a Circle, and runs Circle.area().x = 3.14159...
44Same call on the page, different object: Square.area() runs.y = 4.0
55Copying a reference copies the arrow, not the object. c points at the same Square.c = Square(side=2)
66The variable name does not matter; the object does. Square.area() again.z = 4.0

Getting the subclass back: instanceof patterns

Through a Shape variable you can only call Shape methods, even when the object is a Circle. To use Circle-only members, test and cast in one step with a type pattern: if (s instanceof Circle c) binds c only when the test succeeds. A blind cast like (Circle) s compiles but fails at runtime if the object is something else.

javaMain.java
public class Main {
    public static void main(String[] args) {
        Shape[] shapes = { new Circle(3), new Square(2) };
        for (Shape s : shapes) {
            if (s instanceof Circle c) {
                System.out.println("circle with radius " + c.r);
            } else {
                System.out.println("not a circle");
            }
        }
    }
}

class Shape { }
class Circle extends Shape { double r; Circle(double r) { this.r = r; } }
class Square extends Shape { double side; Square(double side) { this.side = side; } }
Outputcompiled & run with real Java
circle with radius 3.0
not a circle
Error you will hit

ClassCastException: class Square cannot be cast to class Circle

java
public class Main {
    public static void main(String[] args) {
        Shape s = new Square(2);
        Circle c = (Circle) s;
        System.out.println(c.r);
    }
}

class Shape { }
class Circle extends Shape { double r; Circle(double r) { this.r = r; } }
class Square extends Shape { double side; Square(double side) { this.side = side; } }
Exception in thread "main" java.lang.ClassCastException: class Square cannot be cast to class Circle (Square and Circle are in unnamed module of loader com.sun.tools.javac.launcher.MemoryClassLoader @45c7e403)
	at Main.main(Main.java:4)
Why the compiler said that

A cast is a promise to the compiler: "trust me, this Shape is really a Circle." The compiler accepts it because a Shape might be a Circle. At runtime the JVM checks the promise, finds a Square, and throws.

The fix

Test before you cast with if (s instanceof Circle c). Better still, if you keep asking "which subclass is this?", move the behaviour into an overridden method so dispatch answers the question for you.

Only instance methods are dispatched
Fields and static methods are picked by the static type. If a subclass declares a field with the same name as the parent's, it hides it rather than overriding it, and parentVar.field reads the parent's copy. Keep fields private and expose them through methods, and this confusion never arises.
04

Abstract classes

In the dispatch example, new Shape() with an area of 0 made no real sense. Mark the class abstract and it can no longer be instantiated; mark a method abstract (no body, just a semicolon) and every concrete subclass must implement it. An abstract class can still have fields, constructors and fully written methods that subclasses share.

javaMain.java
public class Main {
    public static void main(String[] args) {
        Report[] reports = { new SalesReport(), new StockReport() };
        for (Report r : reports) {
            r.print();
        }
    }
}

abstract class Report {
    // The shared algorithm: fixed order, subclasses fill in the steps
    final void print() {
        System.out.println("== " + title() + " ==");
        System.out.println(body());
        System.out.println("-- end --");
    }

    abstract String title();
    abstract String body();
}

class SalesReport extends Report {
    String title() { return "Sales"; }
    String body()  { return "42 orders, 3 refunds"; }
}

class StockReport extends Report {
    String title() { return "Stock"; }
    String body()  { return "7 items below reorder level"; }
}
Outputcompiled & run with real Java
== Sales ==
42 orders, 3 refunds
-- end --
== Stock ==
7 items below reorder level
-- end --

This shape — a final method in the parent calling abstract steps the children supply — is the Template Method pattern. See it in Design Patterns.

Error you will hit

Shape is abstract; cannot be instantiated

java
public class Main {
    public static void main(String[] args) {
        Shape s = new Shape();
        System.out.println(s.area());
    }
}

abstract class Shape {
    abstract double area();
}
Main.java:3: error: Shape is abstract; cannot be instantiated
        Shape s = new Shape();
                  ^
1 error
Why the compiler said that

An abstract class is deliberately incomplete — area() has no body — so there is no sensible object to build.

The fix

Create a concrete subclass and instantiate that: Shape s = new Circle(1);. The variable can still have the abstract type.

Error you will hit

is not abstract and does not override abstract method

java
public class Main {
    public static void main(String[] args) {
        System.out.println(new Circle(1).name());
    }
}

abstract class Shape {
    abstract double area();
    String name() { return "shape"; }
}

class Circle extends Shape {
    double r;
    Circle(double r) { this.r = r; }
}
Main.java:12: error: Circle is not abstract and does not override abstract method area() in Shape
class Circle extends Shape {
^
1 error
Why the compiler said that

Circle is concrete, so every abstract method it inherits must have a body. It forgot area(). This is the whole point of abstract methods: the compiler will not let a subclass skip them.

The fix

Implement it: @Override double area() { return Math.PI * r * r; } — or declare Circle abstract too if it is only a stepping stone.

05

Interfaces, default methods and multiple interfaces

An interface is a pure contract: "anything that implements me has these methods". A class implements an interface and supplies the bodies. Unlike classes, a class may implement any number of interfaces, which is how Java gets the useful half of multiple inheritance without inheriting two sets of fields. Interface methods are public and abstract by default; fields are always public static final constants.

javaMain.java
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<Payable> bills = List.of(new Invoice(120.0), new Salary("Asha", 3000.0));
        double total = 0;
        for (Payable p : bills) {
            System.out.println(p.label() + ": " + p.amount());
            total += p.amount();
        }
        System.out.println("total: " + total);
    }
}

interface Payable {
    double amount();
    String label();
}

class Invoice implements Payable {
    private final double value;
    Invoice(double value) { this.value = value; }
    public double amount() { return value; }
    public String label()  { return "invoice"; }
}

class Salary implements Payable {
    private final String who;
    private final double monthly;
    Salary(String who, double monthly) { this.who = who; this.monthly = monthly; }
    public double amount() { return monthly; }
    public String label()  { return "salary for " + who; }
}
Outputcompiled & run with real Java
invoice: 120.0
salary for Asha: 3000.0
total: 3120.0
Your turn

Invoice and Salary share no parent class, yet one loop pays both. Add a Refund class whose amount() is negative.

default, static and private interface methods

Since Java 8 an interface can carry code. A default method has a body that implementing classes inherit and may override — this is how List gained sort and forEach without breaking every existing list class. A static method belongs to the interface itself (Comparator.naturalOrder()). A private method (Java 9+) is a helper shared by the default methods.

javaMain.java
public class Main {
    public static void main(String[] args) {
        Greeter en = new English();
        Greeter fr = new French();
        System.out.println(en.greet("Sam"));
        System.out.println(fr.greet("Sam"));
        System.out.println(en.greetLoudly("Sam"));      // inherited default
        System.out.println(Greeter.of("hola").greet("Sam"));   // static factory
    }
}

interface Greeter {
    String hello();                       // abstract: each class decides

    default String greet(String name) {   // shared body
        return hello() + ", " + name;
    }

    default String greetLoudly(String name) {
        return shout(greet(name));
    }

    private String shout(String s) {      // helper, invisible outside
        return s.toUpperCase() + "!";
    }

    static Greeter of(String word) {      // a lambda can implement a one-method interface
        return () -> word;
    }
}

class English implements Greeter {
    public String hello() { return "Hello"; }
}

class French implements Greeter {
    public String hello() { return "Bonjour"; }
    @Override
    public String greet(String name) { return hello() + " " + name + " !"; }
}
Outputcompiled & run with real Java
Hello, Sam
Bonjour Sam !
HELLO, SAM!
hola, Sam
Error you will hit

types Swimmer and Flyer are incompatible (unrelated defaults)

java
public class Main {
    public static void main(String[] args) {
        System.out.println(new Duck().move());
    }
}

interface Swimmer { default String move() { return "swim"; } }
interface Flyer   { default String move() { return "fly"; } }

class Duck implements Swimmer, Flyer { }
Main.java:10: error: types Swimmer and Flyer are incompatible;
class Duck implements Swimmer, Flyer { }
^
  class Duck inherits unrelated defaults for move() from types Swimmer and Flyer
1 error
Why the compiler said that

Both interfaces offer a default move() and neither is more specific than the other. Java will not guess — this is the "diamond problem", and the compiler makes you settle it.

The fix

Override the method in the class. You can still call a specific parent default with InterfaceName.super.method().

java
public class Main {
    public static void main(String[] args) {
        System.out.println(new Duck().move());
    }
}

interface Swimmer { default String move() { return "swim"; } }
interface Flyer   { default String move() { return "fly"; } }

class Duck implements Swimmer, Flyer {
    @Override
    public String move() {
        return Swimmer.super.move() + " and " + Flyer.super.move();
    }
}

Abstract class

  • One per class (extends)
  • Can have instance fields and constructors
  • Any access level on members
  • Use for a family of closely related classes sharing state and code

Interface

  • Many per class (implements A, B, C)
  • No instance state; only constants
  • Members public (private helpers allowed)
  • Use for a capability many unrelated classes can have: Comparable, Runnable, AutoCloseable
In real jobs
Code is written against interfaces almost everywhere: services depend on a PaymentGateway interface, not a concrete class, so tests can pass in a fake and production can swap vendors. Spring's dependency injection is built on exactly this idea. Default to an interface; reach for an abstract class only when you genuinely need shared fields.
06

Sealed types and pattern-matching switch

Ordinary inheritance is open: anyone can add a subclass later. Sometimes you want the opposite — "a payment is a card, a UPI transfer or cash, and nothing else". A sealed interface or class (Java 17+) lists its permitted subtypes with permits. Each permitted subtype must be final, sealed or non-sealed; records are implicitly final, so records plus a sealed interface are a natural pair.

The payoff arrives in a switch (Java 21). A switch can match on type patterns and even record patterns that pull the components out. Because the compiler knows the complete list of subtypes, it checks the switch is exhaustive — no default needed, and adding a new subtype later turns every switch that forgot it into a compile error.

javaMain.java
public class Main {
    public static void main(String[] args) {
        Payment[] payments = {
            new Card("4111222233334444", 250.0),
            new Upi("asha@bank", 99.0),
            new Cash(20.0),
            new Card("5500111122223333", 12000.0),
        };
        for (Payment p : payments) {
            System.out.println(describe(p));
        }
    }

    static String describe(Payment p) {
        return switch (p) {
            case Card c when c.amount() > 10_000 -> "card ending " + c.last4() + ": needs approval";
            case Card c   -> "card ending " + c.last4() + ": " + c.amount();
            case Upi(String id, double amt) -> "UPI " + id + ": " + amt;   // record pattern
            case Cash cash -> "cash: " + cash.amount();
        };
    }
}

sealed interface Payment permits Card, Upi, Cash {
    double amount();
}

record Card(String number, double amount) implements Payment {
    String last4() { return number.substring(number.length() - 4); }
}
record Upi(String id, double amount) implements Payment { }
record Cash(double amount) implements Payment { }
Outputcompiled & run with real Java
card ending 4444: 250.0
UPI asha@bank: 99.0
cash: 20.0
card ending 3333: needs approval

The when guard refines a case. Guarded cases must come before the unguarded case for the same type, because cases are tried top to bottom.

Error you will hit

the switch expression does not cover all possible input values

java
public class Main {
    public static void main(String[] args) {
        System.out.println(describe(new Cash(20.0)));
    }

    static String describe(Payment p) {
        return switch (p) {
            case Card c -> "card";
            case Upi u  -> "upi";
        };
    }
}

sealed interface Payment permits Card, Upi, Cash { }
record Card(double amount) implements Payment { }
record Upi(double amount) implements Payment { }
record Cash(double amount) implements Payment { }
Main.java:7: error: the switch expression does not cover all possible input values
        return switch (p) {
               ^
1 error
Why the compiler said that

Payment is sealed with three permitted types, and the switch handles only two. A switch expression must produce a value for every possible input, so the compiler rejects it. This is the feature working as designed: it found the forgotten case for you.

The fix

Add the missing case Cash c -> "cash";. Resist adding default — it would silence this check for every subtype you add in future.

Sealed hierarchy versus instanceof chains
A pattern-matching switch over a sealed type is the modern, safe replacement for long if (x instanceof A) … else if (x instanceof B) chains. Use overridden methods when each subtype should own its behaviour; use a sealed switch when the behaviour belongs to the caller (formatting, pricing rules, serialisation) and you want the compiler to catch missing cases.
07

Composition over inheritance

Inheritance couples a subclass to the internal details of its parent, not just its public contract. When the parent calls its own overridable methods, a subclass that overrides one of them can be surprised. The classic demonstration: a set that counts how many elements were ever added.

javaMain.java
import java.util.*;

public class Main {
    public static void main(String[] args) {
        CountingSet s = new CountingSet();
        s.addAll(List.of("a", "b", "c"));
        System.out.println("size = " + s.size() + ", counted = " + s.added);
    }
}

class CountingSet extends HashSet<String> {
    int added = 0;

    @Override
    public boolean add(String e) {
        added++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends String> c) {
        added += c.size();
        return super.addAll(c);     // ...which calls add() for each element
    }
}
Outputcompiled & run with real Java
size = 3, counted = 6

Three elements, counted six times. HashSet.addAll is implemented by calling add, which this class also overrides, so every element is counted twice. Nothing in the HashSet documentation you would normally read warns you.

The fix is composition: instead of being a HashSet, the class has one in a private field and forwards calls to it. It now depends only on the Set interface, so internal changes in HashSet cannot break it — and it can wrap any Set, not just a HashSet.

javaMain.java
import java.util.*;

public class Main {
    public static void main(String[] args) {
        CountingSet s = new CountingSet(new TreeSet<>());
        s.addAll(List.of("c", "a", "b"));
        s.add("a");                        // duplicate: still counted as an attempt
        System.out.println(s.items() + " counted = " + s.added());
    }
}

class CountingSet {
    private final Set<String> inner;       // has-a, not is-a
    private int added = 0;

    CountingSet(Set<String> inner) { this.inner = inner; }

    boolean add(String e) {
        added++;
        return inner.add(e);
    }

    void addAll(Collection<String> c) {
        for (String e : c) add(e);         // our own logic, fully under our control
    }

    int added()        { return added; }
    Set<String> items() { return inner; }
}
Outputcompiled & run with real Java
[a, b, c] counted = 4
  • Use inheritance when the subclass truly is a kind of the parent everywhere the parent is used, and the parent was designed to be extended (it documents which methods call which).
  • Use composition when you want to reuse a class's behaviour, add features around it, or swap the part at runtime. This is the default choice.
  • A long chain like Vehicle → Car → ElectricCar → SelfDrivingElectricCar is a warning sign. Capabilities (Chargeable, Autonomous) are usually interfaces plus composed parts.
08

Object methods and final classes

Every class ultimately extends java.lang.Object, so every object has its methods. The three you override most are toString() (text for printing and logs), equals(Object) and hashCode() (value equality — the contract is covered in Collections). getClass() returns the runtime class. Records generate all three for you.

javaMain.java
import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        Money a = new Money(500, "INR");
        Money b = new Money(500, "INR");
        System.out.println(a);                          // calls toString()
        System.out.println(a == b);                     // same object? no
        System.out.println(a.equals(b));                // same value? yes
        System.out.println(a.hashCode() == b.hashCode());
        System.out.println(a.getClass().getSimpleName());
    }
}

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

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

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money m)) return false;
        return amount == m.amount && currency.equals(m.currency);
    }

    @Override
    public int hashCode() { return Objects.hash(amount, currency); }
}
Outputcompiled & run with real Java
500 INR
false
true
true
Money
Your turn

Rewrite Money as record Money(long amount, String currency) { } and check that the output is almost identical (only the toString format changes).

final: closing a class or a method

final class cannot be extended; final on a method means subclasses cannot override it. String, Integer and every record are final, which is part of why they can be trusted to be immutable: no subclass can sneak in a mutable version. Make value classes final, and make any class final unless you have designed it for extension.

Error you will hit

cannot inherit from final

java
public class Main {
    public static void main(String[] args) {
        System.out.println(new FakeMoney());
    }
}

final class Money { }

class FakeMoney extends Money { }
Main.java:9: error: cannot inherit from final Money
class FakeMoney extends Money { }
                        ^
1 error
Why the compiler said that

Money is declared final, so the compiler refuses any extends Money. The same applies to extends String.

The fix

If you need extra behaviour, wrap it (composition) or write a helper method that takes a Money.

Superclass / subclass
The parent a class extends, and the child that extends it. Java allows one superclass.
super
super(...) calls the parent constructor (first line only); super.m() calls the parent's version of a method.
Overriding
Redefining an inherited instance method with the same signature. Checked by @Override.
Overloading
Several methods with the same name but different parameter lists. Chosen at compile time.
Dynamic dispatch
The JVM choosing which override to run from the object's runtime class at call time.
Static vs runtime type
The declared type of a variable (what you may call) versus the class of the object it points at (what runs).
Abstract class
A class that cannot be instantiated and may declare abstract methods subclasses must implement.
Interface
A contract of methods a class implements; a class may implement many. Can carry default, static and private methods.
Sealed type
A class or interface that lists its only permitted subtypes, enabling exhaustive switches.
Composition
Reusing behaviour by holding another object in a field and delegating to it ("has-a" instead of "is-a").
Quick check

Animal a = new Dog(); — Dog overrides speak() and also adds fetch(). Which is true?

Quick check

Why is adding a new record to a sealed interface considered safe?

Frequently asked questions

Does Java support multiple inheritance?
Not for classes: a class can extend only one superclass, which avoids inheriting two conflicting sets of fields. A class can implement any number of interfaces, and if two interfaces supply the same default method the class must override it and choose, for example with Swimmer.super.move().
What is the difference between an abstract class and an interface in Java?
An abstract class can hold instance fields, constructors and methods of any access level, and a class can extend only one. An interface holds no instance state, its methods are public (with default, static and private helpers allowed), and a class can implement many. Prefer interfaces for capabilities; use an abstract class when related subclasses share state.
What is dynamic dispatch in Java?
It is how Java picks which overridden method to run: at call time the JVM looks at the actual object's class, not the declared type of the variable. So Shape s = new Circle(); s.area() runs Circle.area(). Static methods and fields are not dispatched this way; they are chosen by the declared type.

Finish the Java handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.