Command Palette

Search for a command to run...

[Java Basics] Inheritance in Java: extends and super

Inheritance lets one class take over the fields and methods of another and add to them. It is the feature most tutorials reach for first, and it is also the feature most likely to be the wrong tool for the job you are holding.

So this article does two things. It teaches extends, super, protected and final properly, down to the exact order in which constructors run. And it is honest about the cost, because a class hierarchy that looked reasonable on day one is the single hardest thing to unwind six months later.

A chain of class boxes linked upward by extends to java.lang.Object

Every output line and every error message below was produced by compiling and running the code on OpenJDK 21.0.6. Where a rule is enforced by the compiler, the real javac message is quoted rather than paraphrased.

What inheritance is, and the "is a" test

class B extends A says two things at once. It says a B object contains everything an A object contains, and it says a B is an A — it can be used anywhere an A is expected.

That second half is the part people skip, and it is the part that decides whether inheritance is correct. The test is a sentence: can you say "every B is a A" and have it be true of the domain, not just convenient for your code?

RelationshipIs the sentence true?Right tool
SavingsAccount / Accountevery savings account is an accountinheritance
Manager / Employeeevery manager is an employeeinheritance
Car / Enginea car is not an engine, it has onecomposition
Invoice / Bufferan invoice is not a buffer, it uses onecomposition
Stack / ArrayLista stack is not a list — a list lets you insert in the middlecomposition

The last three are all the same mistake wearing different clothes: reaching for extends because the other class already has code you want. That is code reuse, not classification, and composition does it without the side effects. There is a section on that at the end of this article, with both versions of the same program.

Read the rest of this article as the manual for a sharp tool. Learn it thoroughly, then use it sparingly.

extends: what a subclass gets

A subclass declares extends and inherits the superclass's fields and methods, subject to the access modifiers. It can add its own, and it can call the inherited ones as if it had declared them.

class Account {
    String owner;
    double balance;

    Account(String owner, double balance) {
        this.owner = owner;
        this.balance = balance;
    }

    void deposit(double amount) {
        balance += amount;
    }

    String describe() {
        return owner + " has " + balance;
    }
}

class SavingsAccount extends Account {
    double rate;

    SavingsAccount(String owner, double balance, double rate) {
        super(owner, balance);
        this.rate = rate;
    }

    void addInterest() {
        deposit(balance * rate);
    }
}

public class InheritanceBasics {
    public static void main(String[] args) {
        SavingsAccount s = new SavingsAccount("Mai", 1000.0, 0.05);
        s.deposit(500.0);
        s.addInterest();
        System.out.println(s.describe());
        System.out.println("owner field  = " + s.owner);
        System.out.println("rate field   = " + s.rate);
    }
}
Mai has 1575.0
owner field  = Mai
rate field   = 0.05

SavingsAccount never declares owner, balance, deposit or describe, and uses all four. addInterest calls deposit with no qualifier, and it reads balance the same way — inherited members are ordinary members of the subclass.

What crosses the boundary and what does not:

MemberInherited?
public and protected fields and methodsyes
package-private membersonly when both classes are in the same package
private membersno — they exist in the object, but the subclass cannot name them
static fields and methodsyes, and accessible through the subclass name
Constructorsnever
Initializer blocksnot inherited; each class runs its own

The private row deserves a second look, because it trips people up. A private field is still allocated in every subclass instance — the object carries it — but no subclass code can read or write it. The only route in is a public or protected accessor the superclass chose to provide.

Constructors are not inherited

This is the rule that produces the most confusing first error, because the message points at a line you did not think was doing anything.

class Animal {
    String name;

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

class Dog extends Animal {
}

public class CtorNotInherited {
    public static void main(String[] args) {
        Dog d = new Dog("Rex");
        System.out.println(d.name);
    }
}
CtorNotInherited.java:9: error: constructor Animal in class Animal cannot be applied to given types;
class Dog extends Animal {
^
  required: String
  found:    no arguments
  reason: actual and formal argument lists differ in length
CtorNotInherited.java:14: error: constructor Dog in class Dog cannot be applied to given types;
        Dog d = new Dog("Rex");
                ^
  required: no arguments
  found:    String
  reason: actual and formal argument lists differ in length

Two errors from a class with no visible constructor at all. The second one is the direct answer: Dog has no constructor taking a String, because Animal(String) did not come along with the inheritance. Dog got the compiler's default no-argument constructor instead — that is what required: no arguments means.

The first error is the consequence of the same fact. The generated Dog() starts with an implicit super(), Animal has no no-argument constructor, and the call fails. One missing constructor, two errors, neither of them on a line you wrote.

Every class inherits from java.lang.Object

A class with no extends clause is not at the top of anything. It extends java.lang.Object implicitly, so every object in a Java program has the same root.

class Vehicle {
    int wheels = 4;
}

class Car extends Vehicle {
    String model = "Civic";
}

class SportsCar extends Car {
    int topSpeed = 250;
}

public class ObjectRoot {
    public static void main(String[] args) {
        SportsCar sc = new SportsCar();

        Class<?> c = sc.getClass();
        while (c != null) {
            System.out.println(c.getName());
            c = c.getSuperclass();
        }
        System.out.println("after Object: " + Object.class.getSuperclass());

        System.out.println();
        System.out.println("toString  -> " + sc.toString());
        System.out.println("hashCode  -> " + Integer.toHexString(sc.hashCode()));
        System.out.println("equals    -> " + sc.equals(sc) + " / " + sc.equals(new SportsCar()));
        System.out.println("getClass  -> " + sc.getClass().getSimpleName());
    }
}
SportsCar
Car
Vehicle
java.lang.Object
after Object: null

getSuperclass() walks one link at a time and returns null at the top, which is the concrete proof that Object is the end of the chain and that Vehicle — which declares no extends — is nonetheless below it.

That inheritance is why these four methods exist on every reference you will ever hold:

toString  -> SportsCar@6d6f6e28
hashCode  -> 6d6f6e28
equals    -> true / false
getClass  -> SportsCar

The hex suffix is the identity hash and differs from run to run; the rest is stable.

MethodDefault behaviour inherited from Object
toString()the class name, @, and the hex identity hash — exactly what you see above
equals(Object)reference identity, so a.equals(b) is a == b
hashCode()an identity hash, constant for the life of the object
getClass()the runtime class; it is final, so nothing can change what it reports

Object also contributes clone(), notify(), notifyAll() and the wait() overloads. equals returning false for two separately constructed SportsCar objects is the default identity behaviour, not a bug — giving it a value-based meaning is a deliberate act you perform yourself.

A class chain with what each level adds, the real getSuperclass walk, and the error proving constructors are not inherited

Java has single inheritance of classes

A class extends exactly one class. Listing two is not a semantic error the compiler explains politely — the grammar simply ends at the first class name.

class Engine {
    void start() { System.out.println("engine start"); }
}

class Radio {
    void play() { System.out.println("radio play"); }
}

class Car extends Engine, Radio {
}
SingleInheritance.java:9: error: '{' expected
class Car extends Engine, Radio {
                        ^

The caret is on the comma. javac finished reading extends Engine and expected the class body to start.

The restriction exists to avoid the diamond problem: if Car inherited a concrete reset() from two superclasses, there would be no principled way to decide which body runs. Java's answer is that a class has one implementation parent and any number of interface types — which is where multiple inheritance of type lives, and the subject of article 30.

The example above is also a good "is a" test failure. A car is not an engine and is not a radio; it has one of each. Two fields solve it with no language feature at all.

How super(...) chains constructors

This is where beginners get stuck, so it is worth being precise. Every constructor begins by running a superclass constructor. If the first statement is not an explicit super(...) or this(...), the compiler inserts super() for you.

The insertion is not a figure of speech. Compile a subclass with no constructor at all and disassemble it:

class Parent {
    Parent() {
        System.out.println("Parent()");
    }
}

class Child extends Parent {
    int x = 7;
}
class Child extends Parent {
  int x;

  Child();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method Parent."<init>":()V
       4: aload_0
       5: bipush        7
       7: putfield      #7                  // Field x:I
      10: return
}

The generated Child() calls Parent."<init>" before it does anything else, and only then assigns x. Field initializers run after the superclass constructor returns — that ordering matters, and the next example makes it visible.

Trace the whole thing. ConstructorChain.trace prints an incrementing counter, so the numbers in the output are the real execution order and not a claim:

class Base {
    String baseTag = ConstructorChain.trace("Base field initializer");

    Base(String from) {
        ConstructorChain.trace("Base body (arg = " + from + ")");
    }
}

class Middle extends Base {
    String middleTag = ConstructorChain.trace("Middle field initializer");

    Middle() {
        super(ConstructorChain.trace("argument of super(...) in Middle"));
        ConstructorChain.trace("Middle body");
    }
}

class Leaf extends Middle {
    String leafTag = ConstructorChain.trace("Leaf field initializer");

    Leaf() {
        ConstructorChain.trace("Leaf body");
    }
}

public class ConstructorChain {
    static int step = 0;

    static String trace(String msg) {
        System.out.println((++step) + ". " + msg);
        return msg;
    }

    public static void main(String[] args) {
        System.out.println("new Leaf()");
        new Leaf();
    }
}
new Leaf()
1. argument of super(...) in Middle
2. Base field initializer
3. Base body (arg = argument of super(...) in Middle)
4. Middle field initializer
5. Middle body
6. Leaf field initializer
7. Leaf body

Read that against the source and the shape is clear. new Leaf() enters Leaf(), which immediately calls the implicit super(). That enters Middle(), which evaluates the argument expression for super(...) — step 1, the only thing that happens on the way up — and calls Base(String). Base calls Object(). Only now does anything execute in a body: Base's field initializer, then Base's body, then Middle's, then Leaf's.

So the call travels up and the work happens down. For each class the order is: superclass constructor, then that class's field initializers and instance initializer blocks, then that class's constructor body.

new Leaf walking up the super chain to Object and the bodies running back down, numbered against the real output

The practical consequence: when a superclass constructor runs, none of the subclass's fields have been initialized yet. They still hold their default values.

When the superclass has no no-argument constructor

Write a constructor in the subclass, forget super(...), and the invisible super() the compiler inserts has nothing to call.

class Shape {
    String name;

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

class Circle extends Shape {
    double radius;

    Circle(double radius) {
        this.radius = radius;
    }
}
MissingSuper.java:12: error: constructor Shape in class Shape cannot be applied to given types;
    Circle(double radius) {
                          ^
  required: String
  found:    no arguments
  reason: actual and formal argument lists differ in length

found: no arguments is the tell. Nothing in Circle calls Shape with zero arguments, but the compiler did. The fix is to call it yourself: super("circle") or Circle(String name, double radius) { super(name); ... }.

Note the asymmetry this creates. Giving a class a constructor with parameters removes the default no-argument one, and every subclass immediately has to supply super(...). A superclass that keeps a no-argument constructor is easier to extend — which is a design decision, not an accident.

super(...) must be the first statement

The call has to be the very first statement in the constructor. Not the first meaningful one — the first one.

class Square extends Shape {
    double side;

    Square(double side) {
        this.side = side;
        super("square");
    }
}
SuperNotFirst.java:14: error: call to super must be first statement in constructor
        super("square");
             ^

The same rule governs this(...), which delegates to another constructor of the same class. A constructor may start with super(...) or this(...), never both:

class Rect extends Shape {
    double w, h;

    Rect(double w, double h) {
        super("rect");
        this.w = w;
        this.h = h;
    }

    Rect(double side) {
        super("square");
        this(side, side);
    }
}
ThisAndSuper.java:20: error: call to this must be first statement in constructor
        this(side, side);
            ^

That is the correct design anyway: Rect(double) should call this(side, side) and let that constructor do the single super(...). Exactly one superclass constructor runs per object, no matter how many this(...) hops are chained in front of it.

The "first statement" rule is what JDK 21 enforces, and it is the rule to learn. Java 25 finalised JEP 513, Flexible Constructor Bodies, which permits statements before super(...) provided they do not read or write the instance under construction. The examples in this article were compiled on OpenJDK 21.0.6, where the rule is absolute.

Reaching the superclass with super.method() and super.field

Inside a subclass, super. names the superclass's version of a member. It is how you extend behaviour rather than replace it:

class Report {
    String title;

    Report(String title) {
        this.title = title;
    }

    String render() {
        return "== " + title + " ==";
    }
}

class SalesReport extends Report {
    double total;

    SalesReport(double total) {
        super("Sales");
        this.total = total;
    }

    String render() {
        return super.render() + "\n   total: " + total;
    }
}
== Sales ==
   total: 1250.5

SalesReport.render reuses the superclass output and appends to it. Drop the super. and the method calls itself:

class SalesReport extends Report {
    String render() {
        return render() + " + total";
    }
}
Exception in thread "main" java.lang.StackOverflowError
	at SalesReport.render(ForgotSuper.java:9)
	at SalesReport.render(ForgotSuper.java:9)
	at SalesReport.render(ForgotSuper.java:9)
	at SalesReport.render(ForgotSuper.java:9)
	at SalesReport.render(ForgotSuper.java:9)

A stack trace of one repeated line is the signature of exactly this mistake. super. is not decoration; it is what makes the call go to a different method.

super.someField works the same way and is the only way to read a superclass field that a subclass field of the same name has hidden — which is the subject of the next section. What super. cannot do is skip a level: there is no super.super.render(), because a class is only allowed to depend on its immediate parent's contract.

Redefining a method the superclass already declares is overriding, and its full semantics — which body actually runs, @Override, and what happens through a superclass-typed reference — belong to article 28. Here it is only scaffolding for showing what super. reaches.

Field hiding is not overriding

This is the trap in this article, and it is worth slowing down for. When a subclass declares a field with the same name as a superclass field, it does not replace it. Both fields exist on the object, and which one you read is decided by the declared type of the reference, at compile time.

import java.lang.reflect.Field;

class Config {
    String source = "base";
    int timeout = 30;

    String fromBase() {
        return "inside Config: source=" + source + " timeout=" + timeout;
    }
}

class DevConfig extends Config {
    String source = "dev";

    String fromSub() {
        return "inside DevConfig: source=" + source + " super.source=" + super.source
                + " timeout=" + timeout;
    }
}

public class FieldHiding {
    static void readAsBase(Config c) {
        System.out.println("through a Config reference: source=" + c.source);
    }

    public static void main(String[] args) throws Exception {
        DevConfig dev = new DevConfig();
        Config asBase = dev;

        System.out.println("same object? " + (dev == asBase));
        System.out.println("dev.source     = " + dev.source);
        System.out.println("asBase.source  = " + asBase.source);
        readAsBase(dev);
        System.out.println(dev.fromBase());
        System.out.println(dev.fromSub());

        System.out.println();
        System.out.println("slots on one object:");
        for (Class<?> c = dev.getClass(); c != Object.class; c = c.getSuperclass()) {
            for (Field f : c.getDeclaredFields()) {
                f.setAccessible(true);
                System.out.println("  " + c.getSimpleName() + "." + f.getName()
                        + " = " + f.get(dev));
            }
        }
    }
}
same object? true
dev.source     = dev
asBase.source  = base
through a Config reference: source=base
inside Config: source=base timeout=30
inside DevConfig: source=dev super.source=base timeout=30

slots on one object:
  DevConfig.source = dev
  Config.source = base
  Config.timeout = 30

dev and asBase are the same object — dev == asBase prints true — and reading source through them gives different strings. Nothing about the object changed; only the type of the name used to reach it.

The reflection dump at the end shows why: the object genuinely carries DevConfig.source and Config.source side by side. Hiding does not overwrite a slot, it adds one. timeout appears once because only Config declares it, so both references agree about it.

One object holding both the Config field slots and the DevConfig one, with two references selecting different source slots

Method calls do not behave like this at all, and the contrast is the whole subject of article 28. For now, the operative rule is the plain one:

⚠️ Never give a subclass field the same name as a superclass field. There is no case where the two-slot behaviour is what you wanted, and the resulting bug reads as "the value I set is not the value I get".

The accident usually happens in a constructor. Writing String source; in the subclass because the field "should be there" quietly creates the second slot, the constructor fills the subclass copy, and every inherited superclass method keeps reading the empty superclass copy — which prints null.

protected: what a subclass in another package can touch

protected exists for exactly one purpose: to expose a member to subclasses without exposing it to the world. It only means anything once the subclass is in a different package, so the demonstration needs a real two-package tree:

src/com/example/base/Employee.java
src/com/example/hr/Manager.java
package com.example.base;

public class Employee {
    private String id = "E-001";
    protected String name;
    protected double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    protected void raise(double pct) {
        salary += salary * pct;
    }

    public String toString() {
        return name + " (" + id + ") earns " + salary;
    }
}
package com.example.hr;

import com.example.base.Employee;

public class Manager extends Employee {
    private int reports;

    public Manager(String name, double salary, int reports) {
        super(name, salary);
        this.reports = reports;
    }

    public void annualReview() {
        raise(0.10);
        salary += reports * 500;
        System.out.println("reviewed " + name + ", salary now " + salary);
    }
}
reviewed Linh, salary now 68000.0
Linh (E-001) earns 68000.0

Manager is in com.example.hr and reads salary, writes salary and calls raise — all protected, all across a package boundary. It cannot touch id:

src/com/example/hr/Intern.java:11: error: id has private access in Employee
        System.out.println(id);
                           ^

Yet id is clearly there: toString printed E-001 for a Manager. The field is inherited into the object and hidden from the subclass's code at the same time.

Drop the extends and the access disappears with it. A plain class in the same foreign package gets nothing:

package com.example.hr;

import com.example.base.Employee;

public class Auditor {
    public void audit(Employee e) {
        System.out.println(e.salary);
    }
}
src/com/example/hr/Auditor.java:7: error: salary has protected access in Employee
        System.out.println(e.salary);
                            ^

There is one more rule, and it surprises almost everyone. A subclass in another package may reach a protected member only through a reference of its own type, not through a superclass-typed one:

package com.example.hr;

import com.example.base.Employee;

public class Payroll extends Employee {
    public Payroll(String name, double salary) {
        super(name, salary);
    }

    public void compare(Employee other) {
        System.out.println(this.salary);
        System.out.println(other.salary);
    }
}
src/com/example/hr/Payroll.java:12: error: salary has protected access in Employee
        System.out.println(other.salary);
                                ^

this.salary compiled. other.salary did not, and other is an Employee — the very class that declared the field. The rule grants a subclass access to its own inherited state, not to the state of arbitrary instances of its parent.

ModifierSame classSame packageSubclass, other packageEverywhere
privateyesnonono
package-private (no modifier)yesyesnono
protectedyesyesyes, through a reference of its own typeno
publicyesyesyesyes

Note the second column: protected includes package access. Inside com.example.base, an unrelated class reads salary without any inheritance at all — same package, no inheritance, protected read: 42000.0. protected is strictly wider than package-private, never narrower.

Every protected member is a promise to every future subclass, and it is a promise that is much harder to withdraw than a private one. Prefer private with a protected accessor when the subclass needs a value but not the field.

final: blocking inheritance and overriding

Article 25 introduced final as a modifier that stops reassignment. On classes and methods it does something different: it stops extension.

A final class cannot be a superclass:

final class Money {
    final double amount;

    Money(double amount) {
        this.amount = amount;
    }
}

class Discount extends Money {
    Discount(double amount) {
        super(amount);
    }
}
FinalClass.java:9: error: cannot inherit from final Money
class Discount extends Money {
                       ^

The same message is what you get from class MyString extends String:

ExtendString.java:1: error: cannot inherit from final String
public class ExtendString extends String {
                                  ^

String is final on purpose. Its immutability, the string pool and its hashCode cache are only safe if no subclass can interfere, and the language guarantees that by refusing the extends.

A final method cannot be redefined by a subclass:

class Session {
    private final String token = "abc123";

    public final String token() {
        return token;
    }
}

class FakeSession extends Session {
    public String token() {
        return "anything";
    }
}
FinalMethod.java:10: error: token() in FakeSession cannot override token() in Session
    public String token() {
                  ^
  overridden method is final

That is the mechanism behind Object.getClass() — it is final, so no class can lie about its own runtime type.

You writeYou are saying
final class Xthis class's behaviour is fixed; extend it and you get a compile error
final void m()subclasses may exist, but this method's body is not negotiable
private void m()not visible to subclasses, so effectively not overridable either

Marking a class final is not defensive posturing; it is the decision to keep the freedom to change the class later. Which is exactly the problem the next section describes.

The fragile base class problem

Here is the cost, in the smallest form that still shows it. A Mailer sends mail; a CountingMailer subclass counts what it sends.

class Mailer {
    void send(String to) {
        System.out.println("  -> " + to);
    }

    void sendAll(String[] recipients) {
        for (String r : recipients) {
            System.out.println("  -> " + r);
        }
    }
}

class CountingMailer extends Mailer {
    int sent = 0;

    void send(String to) {
        sent++;
        super.send(to);
    }

    void sendAll(String[] recipients) {
        sent += recipients.length;
        super.sendAll(recipients);
    }
}

One send and a batch of two gives the right answer:

  -> a@example.com
  -> b@example.com
  -> c@example.com
sent = 3

Now somebody tidies up Mailer. The duplicated print in sendAll becomes a call to send. The public API is untouched, no signature moves, and no subclass is edited:

    void sendAll(String[] recipients) {
        for (String r : recipients) {
            send(r);
        }
    }
  -> a@example.com
  -> b@example.com
  -> c@example.com
sent = 5

Identical mail, and the counter is now wrong. CountingMailer.sendAll adds 2 and then Mailer.sendAll calls send twice, which adds 2 more. The batch is counted twice.

Nothing here is a bug in either class read on its own. The subclass depended on a fact the superclass never promised — that sendAll did not go through send — and a refactoring inside the superclass silently changed the answer. That is the fragile base class problem: a subclass is coupled to its superclass's implementation, not only to its API, and there is no syntax for the superclass author to declare which internal calls are part of the contract.

It gets worse in the real world, where the superclass lives in a library you upgrade rather than a file you can read in the same commit. The JDK has exactly this shape. Write the same counting subclass twice, once over HashSet and once over ArrayList:

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

    public boolean add(E e) {
        added++;
        return super.add(e);
    }

    public boolean addAll(Collection<? extends E> c) {
        added += c.size();
        return super.addAll(c);
    }
}

CountingList extends ArrayList with a body identical line for line. One add plus a batch of three goes into each:

HashSet   subclass: size=4 added=7
ArrayList subclass: size=4 added=4

Four elements either way, and the HashSet subclass counted seven. HashSet does not implement addAll at all — reflection reports it as declared in java.util.AbstractCollection, whose version loops calling add, so the subclass's add fires three extra times. ArrayList declares its own addAll and never calls add.

Identical subclass code, one JDK, two different answers, and nothing in either class's published API says which one you are getting. That is why "prefer composition to inheritance" is standard advice rather than a stylistic preference.

Composition instead of inheritance

Composition means holding the other object in a field and calling it, instead of becoming it. The counting mailer, written the other way, with the refactored Mailer unchanged:

class CountingMailerBox {
    private final Mailer inner = new Mailer();
    int sent = 0;

    void send(String to) {
        sent++;
        inner.send(to);
    }

    void sendAll(String[] recipients) {
        for (String r : recipients) {
            send(r);
        }
    }
}
inheritance -> sent = 5
composition -> sent = 3

Same program, same Mailer, and the composed version is correct. It never calls inner.sendAll, so however Mailer chooses to implement batching internally is none of its business. The dependency is on the two methods it actually calls, and nothing else.

The second effect is control over the API. Inheriting drags every public member of the superclass into your class whether it makes sense there or not:

class Buffer {
    private StringBuilder sb = new StringBuilder();

    void write(String s) { sb.append(s); }
    void clear() { sb.setLength(0); }
    String content() { return sb.toString(); }
}

class InvoiceInherits extends Buffer {
    void addLine(String item, double price) {
        write(item + "  " + price + "\n");
    }
}

class InvoiceComposes {
    private final Buffer buf = new Buffer();

    void addLine(String item, double price) {
        buf.write(item + "  " + price + "\n");
    }

    String render() { return buf.content(); }
}

clear() was never meant to be part of an invoice, but InvoiceInherits published it anyway, and a caller can wipe a half-built invoice:

inherited invoice:
Tea  3.0
composed invoice:
Coffee  4.5
Tea  3.0

The composed version does not have the method to misuse:

NoClear.java:5: error: cannot find symbol
        b.clear();
         ^
  symbol:   method clear()
  location: variable b of type InvoiceComposes

An honest comparison, because inheritance is not always the loser:

InheritanceComposition
Relationship expressedis ahas a
Coupled tothe superclass's implementation, including its internal callsonly the methods you call
API surfaceevery public and protected member comes alongexactly what you choose to expose
Superclass changescan silently change behaviour, as abovecannot reach you
Substitutabilitya subclass can be passed wherever the superclass is expectedyou get none for free
Delegation costnone to writeone forwarding method per operation you re-expose

The last row is the real trade. Composition makes you write forwarding methods, and for a wide API that is genuinely tedious. Inheritance is the right answer when the "is a" sentence is true and you want substitutability — a Manager that can be passed to any method taking an Employee is the whole point. Reach for composition when you only wanted the code.

Common mistakes

Expecting constructors to be inherited. new Dog("Rex") after class Dog extends Animal with only Animal(String) in scope gives constructor Dog in class Dog cannot be applied to given types, because Dog has only the default no-argument constructor. Declare the constructor you want and forward with super(name).

Forgetting super(...) when the superclass has no no-argument constructor. The message names the superclass and says found: no arguments, which reads oddly on a subclass constructor that clearly has one. It is describing the super() the compiler inserted.

Putting super(...) anywhere but first. call to super must be first statement in constructor. If you need to compute a value for it, compute it in the argument expression or in a private static helper — those are evaluated before the superclass constructor runs, as step 1 of the trace showed.

Redeclaring a superclass field in the subclass. No error, no warning, two slots, and dev.source versus asBase.source printing different strings for one object. If you want the subclass to change the value, assign to the inherited field instead of declaring a new one.

Using extends because the superclass has code you want. The Invoice that extended Buffer inherited clear() and published it to every caller. If the sentence "an invoice is a buffer" is false, hold a Buffer in a field.

Reaching for protected by reflex. Every protected member is part of the contract with all future subclasses, and widening it later is easy while narrowing it is a breaking change. Start private.

FAQ

Does a subclass inherit constructors in Java?

No. Constructors are the one thing extends never carries across, because a constructor is tied to the class it names. A subclass either declares its own constructors or gets the compiler's default no-argument one. That is why class Dog extends Animal { } with Animal(String) in the superclass fails on new Dog("Rex") with constructor Dog in class Dog cannot be applied to given types.

What does super() do if I do not write it?

The compiler inserts super() as the first statement of any constructor that does not start with an explicit super(...) or this(...). It is a real call — disassembling a subclass with no constructor shows invokespecial Method Parent."<init>":()V as the first instruction. If the superclass has no no-argument constructor, that inserted call does not compile, and the error appears on your constructor's signature line.

Why does javac say "constructor X in class X cannot be applied to given types"?

Two different situations produce it. If the class named is the superclass and the message says found: no arguments, the implicit super() failed — add an explicit super(...) with the right arguments. If the class named is the one you are instantiating and required: no arguments, you called a constructor the class does not have, usually because you expected it to inherit one.

Can a Java class extend more than one class?

No. class Car extends Engine, Radio does not even parse — javac reports '{' expected with the caret on the comma. A class has exactly one superclass, defaulting to java.lang.Object. Multiple inheritance of type comes from interface, which a class can implement any number of, and which article 30 covers.

What is the difference between hiding a field and overriding a method?

A subclass field with the same name as a superclass field hides it: both slots exist on the object, and which one is read is decided by the declared type of the reference at compile time. The same object printed dev through a DevConfig reference and base through a Config reference. Methods resolve by the object's actual class instead, which is why the two look similar in source and behave nothing alike — article 28 covers that half in full.

When should I use composition instead of inheritance?

Whenever the "is a" sentence is false, and often even when it is true. Use inheritance when a subclass genuinely must be usable everywhere the superclass is, and when you control both classes. Use composition when you only wanted to reuse the code — it does not drag the superclass's whole API into yours, and it cannot be broken by a refactoring inside the class you are reusing, as the counter that jumped from 3 to 5 showed.

Conclusion

Inheritance is two mechanisms wearing one keyword. extends copies a set of members into your class, and it declares an "is a" relationship that lets your object stand in for the superclass. The first is easy to reach for and the second is what actually makes it correct.

The details that decide whether your code works are all in this article's outputs. Constructors do not cross the boundary, so new Dog("Rex") fails on a class you never gave a constructor. super(...) runs first, always, and the chain walks up to Object before a single body executes — steps 1 through 7 in the trace, in that exact order. A redeclared field creates a second slot and a same-object read that returns two different values. And a superclass that quietly refactors sendAll to call send can turn a correct subclass into a wrong one without either file looking broken.

Next in this series: polymorphism — overriding versus overloading. Method overriding is where inheritance stops being about reuse and starts being about substitution: which body runs when you call a method through a superclass reference, what @Override buys you, and why overloading — chosen by the compiler from the static types — is a completely different mechanism that only looks similar.

Related Posts

[Java Basics] Variable Scope in Java: Local, Field and Static

Variable scope in Java explained: local variables, parameters, instance fields and static fields, block scope, shadowing, definite assignment and lifetime, with every cannot find symbol error reproduced on JDK 21.

[Java Basics] Recursion in Java: How It Works and When to Use It

How recursion works in Java: the base case and the recursive case, factorial traced frame by frame, a real StackOverflowError from a missing base case, recursion depth and -Xss, why naive Fibonacci needs 2692537 calls for fib(30) while memoisation needs 59, recursion versus iteration, and why the JVM does not optimise tail calls.

[Java Basics] Conditional Statements in Java: if, else if, else and switch

Conditional statements in Java: the if / else if / else chain, why braces matter, the dangling else, boolean-only conditions, switch with break and fall-through, arrow labels, switch expressions with yield, and pattern matching in Java 21.

[Java Basics] Methods in Java: Declaring and Calling Them

How to declare and call a method in Java: the parts of a declaration, static versus instance methods and the non-static method cannot be referenced from a static context error, the return statement, the call stack and StackOverflowError, reading a stack trace, Javadoc, and the real compiler errors beginners hit.