Nexus Software has just told BiblioTech about a new development: the technical library does not lend only books. It also lends specialised magazines —returned within a week because they circulate a lot— and training DVDs, lent for three days and whose late rate is double, because there are few copies. With what you know so far, the only way out would be to duplicate the Book class twice, changing two numbers and the name: three almost identical classes, with the same availability logic copied three times and three places to fix every bug. Inheritance is the mechanism Java uses to avoid exactly that: it lets you define a class as a specialisation of another, inheriting everything it already has and declaring only what differs. It is OOP's most powerful pillar and also the easiest to misuse, so this lesson teaches you both how to apply it and how to know when not to.

Contents

  1. The "is a" relationship
  2. extends: syntax and first example
  3. What is inherited and what is not
  4. super: the superclass constructor
  5. super: accessing superclass members
  6. Order of construction in a hierarchy
  7. Method overriding and @Override
  8. Overriding versus overloading
  9. final on classes and methods
  10. Every class inherits from Object
  11. Inheritance versus composition
  12. Deep hierarchies and the fragile base class
  13. BiblioTech: the hierarchy of lendable materials
  14. Common Mistakes and Tips
  15. Exercises

  1. The "is a" relationship

Inheritance models a very specific relationship: "X is a Y". If the sentence sounds natural and is always true, inheritance is a candidate:

  • A book is a lendable material. ✔
  • A magazine is a lendable material. ✔
  • A DVD is a lendable material. ✔
  • A loan is a book. ✘ (a loan has a book)
  • An employee is a book. ✘

Vocabulary:

Term Synonyms Meaning
Superclass base class, parent class The general class that is inherited from
Subclass derived class, child class The specialised class that inherits
Inheritance extension The mechanism that joins them

And one rule Java imposes that is worth knowing from the start: a class can only extend one superclass. There is no multiple inheritance of classes (there is of interfaces, lesson 04-01). This restriction avoids ambiguities famous in other languages.

  1. extends: syntax and first example

public class Material {
    public String title;
    public boolean available;

    public void lend() {
        available = false;
    }
}
public class Magazine extends Material {
    public int number;          // its own field, on top of the inherited ones
}

With that single line, Magazine has title, available and lend() without having written them:

Magazine m = new Magazine();
m.title      = "Java Magazine";   // inherited
m.number     = 42;                // its own
m.lend();                         // inherited
System.out.println(m.available);  // false

A subclass is equal to or bigger than its superclass: never smaller. It can add fields, add methods and change the behaviour of the inherited ones, but it cannot remove anything.

  1. What is inherited and what is not

A table that settles most of the doubts:

Superclass element Inherited? Nuance
public and protected fields Yes Directly accessible from the subclass
private fields They exist, but are not accessible The object contains them; the subclass must use methods to reach them
Package (default) fields and methods Only if the subclass is in the same package See 03-07
public and protected methods Yes They can be overridden
static methods Inherited, but not overridden They are "hidden" (lesson 03-06)
Constructors No They must be declared in each subclass, although they can be invoked with super(...)
Initialisation blocks They run as part of construction They are not "inherited" as members

The point that confuses most is private fields. A Magazine object does contain Material's private fields —they take up memory and are initialised—, but Magazine's code cannot name them. It must use whatever methods the superclass exposes. It is not an arbitrary limitation: it is encapsulation (03-07), and it lets you change Material's internal representation without breaking its children.

The other point is constructors: they are not inherited. If Material has a constructor with three parameters, new Magazine(a, b, c) does not work by magic; Magazine must declare its own constructor.

  1. super: the superclass constructor

super(...) invokes a constructor of the superclass. And it is mandatory in the following sense:

Every subclass constructor starts by calling a constructor of its superclass. If you do not write it yourself, the compiler inserts an implicit call to super() with no arguments.

public class Material {
    public final String title;
    public final String reference;
    public boolean available;

    public Material(String title, String reference) {
        this.title     = title;
        this.reference = reference;
        this.available = true;
    }
}
public class Magazine extends Material {
    public final int number;

    public Magazine(String title, String reference, int number) {
        super(title, reference);     // FIRST statement, mandatory here
        this.number = number;
    }
}

Rules, twins of the this(...) ones you saw in 03-04:

  1. super(...) must be the first statement of the constructor.
  2. super(...) and this(...) cannot coexist in the same constructor: if you use this(...), the chain will end at some constructor that calls super(...).
  3. If the superclass has no no-argument constructor and the subclass does not explicitly call super(...), there is a compile error:
public class Magazine extends Material {
    public Magazine() { }   // ERROR
}
error: constructor Material in class Material cannot be applied to given types;
  required: String,String
  found:    no arguments

This very frequent error has a clear reading: "you have created a subclass of something that demands a title and a reference, but you are not giving them to it".

  1. super: accessing superclass members

super also serves, outside constructors, to reach a superclass member that the subclass has overridden:

public class Material {
    public String describe() {
        return title + " (ref. " + reference + ")";
    }
}

public class Magazine extends Material {
    public int number;

    @Override
    public String describe() {
        return super.describe() + " - number " + number;     // reuses and extends
    }
}

This pattern —call super.method() and add something— is one of the most valuable uses of inheritance: you do not duplicate the superclass's logic, you extend it.

Magazine m = new Magazine("Java Magazine", "REV-2024-42", 42);
System.out.println(m.describe());
// Java Magazine (ref. REV-2024-42) - number 42

  1. Order of construction in a hierarchy

When you create a subclass object, construction goes top down: first the inherited part is built, then its own. Check it with traces:

public class Material {
    public Material() {
        System.out.println("2. Material constructor");
    }
    { System.out.println("1. Material instance block"); }
}

public class Book extends Material {
    public Book() {
        super();                                   // implicit if not written
        System.out.println("4. Book constructor");
    }
    { System.out.println("3. Book instance block"); }
}
new Book();

Output:

1. Material instance block
2. Material constructor
3. Book instance block
4. Book constructor
flowchart TD
    A["new Book()"] --> B["Book constructor
    calls super() as its first statement"]
    B --> C["Material blocks and initialisers"]
    C --> D["Body of the Material constructor"]
    D --> E["Book blocks and initialisers"]
    E --> F["Body of the Book constructor"]
    F --> G["Complete object"]

This order explains the tip left pending in 03-04: do not call overridable methods from a constructor. If Material called, in its constructor, a method that Book overrides, Book's code would run before Book's fields were initialised, and it would see nulls and zeros. Demonstration:

public class Material {
    public Material() {
        System.out.println("Material builds: " + describe());       // DANGER
    }
    public String describe() { return "generic material"; }
}

public class Book extends Material {
    private final String author;

    public Book(String author) {
        super();
        this.author = author;
    }

    @Override
    public String describe() { return "book by " + author; }
}
new Book("Joshua Bloch");
// Material builds: book by null          <-- author not assigned yet

The author field is final and will end up holding "Joshua Bloch", but at the instant of the call it is still null. Practical rule: from a constructor, call only private, static or final methods.

  1. Method overriding and @Override

To override is to redefine an inherited method in the subclass, with the same signature, in order to change its behaviour.

public class Material {
    public int getLoanDays() {
        return 15;
    }
}

public class Dvd extends Material {
    @Override
    public int getLoanDays() {
        return 3;                 // DVDs circulate faster
    }
}

Rules for a valid override:

Element Rule
Name and parameters Identical (same signature)
Return type The same, or a subtype of the original (covariant return)
Visibility The same or more permissive (protected → public yes; public → private no)
final method Cannot be overridden
static method Not overridden, it is hidden (03-06)
private method Not inherited, so not overridden

The @Override annotation is not mandatory, but you should always add it. Its job is to ask the compiler to verify that you really are overriding something. Without it, a typo becomes a new method that nobody calls:

public class Dvd extends Material {
    public int getLoanDay() {          // careful! "Day" without the S
        return 3;
    }
}

That code compiles perfectly and overrides nothing: DVDs will keep having a 15-day term, and finding out why will cost you an afternoon. With @Override in front, the compiler fails immediately:

error: method does not override or implement a method from a supertype

An annotation you write in a second that prevents a whole class of silent errors. Annotations in general are studied in lesson 10-02.

  1. Overriding versus overloading

This is one of the most persistent confusions for people learning Java, made worse by how similar the two terms sound. The table makes it clear:

Aspect Overloading Overriding
Where it happens In the same class (or while inheriting) Between superclass and subclass
Signature Different parameter list Identical
Name The same The same
Return type Can change freely The same or a subtype
When it is decided At compile time, from the declared type At run time, from the actual object
Annotation None @Override
What it is for Offering variants of the same operation Specialising inherited behaviour
Example calculateFine() and calculateFine(int) Dvd.getLoanDays() over Material.getLoanDays()

An example with both at once:

public class Material {
    public double calculateFine(int daysLate) { return daysLate * 0.25; }
}

public class Dvd extends Material {

    @Override
    public double calculateFine(int daysLate) {         // OVERRIDING
        return daysLate * 0.50;
    }

    public double calculateFine(int daysLate, double discount) {   // OVERLOADING
        return calculateFine(daysLate) * (1 - discount);
    }
}

The most important row of the table is "when it is decided": overloading is resolved by the compiler looking at the types written in the code; overriding is resolved by the JVM at run time looking at the actual object. That difference is the heart of polymorphism, and it is the subject of the next lesson.

  1. final on classes and methods

final has three uses in Java, and you already know two:

Applied to Means
Variable or field It cannot be reassigned (modules 1 and 03-04)
Method It cannot be overridden in any subclass
Class It cannot be extended: it admits no subclasses
public class Material {
    /** The legal cap calculation cannot be altered in any subclass. */
    public final double applyCap(double amount) {
        return Math.min(amount, MAX_FINE);
    }
}
public final class Loan {       // nobody can create a "SubLoan"
    // ...
}

What is it for? To protect invariants. If a rule must always hold, without exception, a final method stops a subclass from skipping it. Java's most famous final classes are String and Integer: if you could extend String and override its methods, the security of the whole language would collapse.

The professional advice, formulated by Joshua Bloch in Effective Java (yes, the book in BiblioTech's catalog): design for inheritance and document it, or prohibit it. A class that has not been explicitly designed to be extended is better declared final.

  1. Every class inherits from Object

A rule of the language you have probably already noticed: if a class does not declare extends, it implicitly inherits from java.lang.Object.

public class Material { }
// is exactly equivalent to
public class Material extends Object { }

That is why you can write this without having defined anything:

Book b = new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018);
System.out.println(b.toString());   // com.nexussoftware...Book@1b6d3586
System.out.println(b.equals(b));    // true
System.out.println(b.hashCode());   // 460141958

BiblioTech's real hierarchy is therefore this:

classDiagram
    Object <|-- Material
    Material <|-- Book
    Material <|-- Magazine
    Material <|-- Dvd
    Object <|-- Employee
    Object <|-- Loan
    class Object {
        +toString() String
        +equals(Object) boolean
        +hashCode() int
        +getClass() Class
    }

The methods Object gives away do work, but their default behaviour is rarely the one you want: toString() prints a cryptic Book@1b6d3586 and equals compares memory addresses, so two books with the same ISBN are different. Overriding them properly is the subject of lesson 03-09, which closes this module.

  1. Inheritance versus composition

Inheritance is so convenient that it gets abused. The alternative is composition: instead of "being a", the object has a collaborator to which it delegates the work.

Criterion Inheritance Composition
Relationship "is a" "has a"
Syntax class B extends A class B { private A a; }
Coupling Strong: B depends on A's internal details Weak: B only uses A's public API
Decided at Compile time (never changes) Run time (the collaborator can be swapped)
Reuses All of A's implementation, whether it wants to or not Only what it chooses to use
Risk Changes in A break B Low

A case where inheritance would be a mistake. Imagine you want Loan to reuse Book's code so as not to repeat the title and the reference, and you write:

public class Loan extends Book {        // DESIGN ERROR
    public Employee employee;
    public int elapsedDays;
}

It compiles. And it is a disaster, for three reasons:

  1. The sentence is false. A loan is not a book; a loan has a book. If the "is a" sentence sounds odd when you say it out loud, do not use inheritance.
  2. It inherits absurd operations. loan.lend() and loan.returnItem() now exist, and they mean nothing. You are enlarging the public surface with meaningless methods.
  3. It breaks polymorphism. Any code that receives a Book would accept a Loan, and a catalog of books could end up containing loans.

The correct form is the one you already have:

public class Loan {              // no extends
    public final Material material;    // HAS a material
    public final Employee employee;    // HAS an employee
}

A practical rule constantly quoted in the industry: prefer composition over inheritance. Use inheritance only when the subclass is genuinely substitutable for the superclass in any context —known as the Liskov substitution principle— and when you want to take advantage of polymorphism.

  1. Deep hierarchies and the fragile base class

Two problems that show up in real projects and are worth anticipating:

Deep hierarchies. Chains like Object → Material → PhysicalMaterial → PrintedMaterial → Publication → Periodical → Magazine mean that understanding Magazine requires reading six files, that a change at any level propagates downwards and that it is impossible to know where a particular method comes from. A healthy criterion: two or three levels at most; if you need more, what you are probably after is composition or interfaces (04-01).

Fragile base class problem. A subclass depends not only on what the superclass does, but on how it does it. An innocuous internal change can break it:

public class Material {
    public int registeredLoans;

    public void register()            { registeredLoans++; }
    public void registerSeveral(int n) {
        for (int i = 0; i < n; i++) {
            register();                      // implementation detail
        }
    }
}

public class Dvd extends Material {
    public int timesRegisteredFromDvd;

    @Override
    public void register() {
        timesRegisteredFromDvd++;
        super.register();
    }

    @Override
    public void registerSeveral(int n) {
        timesRegisteredFromDvd += n;         // adds n...
        super.registerSeveral(n);            // ...and super calls register() n times
    }
}

dvd.registerSeveral(3) increments timesRegisteredFromDvd by 6, not by 3: the superclass internally calls a method the subclass has overridden. And if tomorrow somebody optimises registerSeveral to do registeredLoans += n without calling register(), the subclass's counter will become 3. The same code, a different result, without having touched the subclass. That is the fragility: the subclass depends on internal details nobody promised to keep.

The defence: document exhaustively which methods the base class calls internally, declare final whatever must not be touched, and prefer composition when you do not need polymorphism.

  1. BiblioTech: the hierarchy of lendable materials

Let us apply everything to the project. The target model:

classDiagram
    class Material {
        +String title
        +String reference
        +boolean available
        +MAX_FINE double
        +MINOR_THRESHOLD int
        +getLoanDays() int
        +getDailyRate() double
        +calculateDaysLate(int) int
        +calculateFine(int) double
        +classifySeverity(int) String
        +lend() void
        +returnItem() void
        +describe() String
        +getType() String
    }
    class Book {
        +String author
        +int publicationYear
        +getIsbn() String
    }
    class Magazine {
        +int number
        +String frequency
    }
    class Dvd {
        +int durationMinutes
    }
    Material <|-- Book
    Material <|-- Magazine
    Material <|-- Dvd

The base class Material

package com.nexussoftware.bibliotech.domain;

/**
 * Lendable material from Nexus Software's catalog.
 *
 * It defines what is common to every format: identification, availability and
 * the fine calculation. The subclasses only declare how they differ:
 * the loan term and the daily rate.
 */
public class Material {

    /** Standard loan term, in days. Books use it. */
    public static final int LOAN_DAYS = 15;

    /** Standard rate per day late, in euros. */
    public static final double DAILY_RATE = 0.25;

    /** Maximum amount of a fine, common to every material. */
    public static final double MAX_FINE = 20.0;

    /** Maximum delay, in days, considered minor. */
    public static final int MINOR_THRESHOLD = 7;

    public static int materialsCreated = 0;

    public final String title;
    public final String reference;
    public boolean      available;

    public Material(String title, String reference, boolean available) {
        this.title     = (title == null || title.isBlank()) ? "Untitled" : title.trim();
        this.reference = (reference == null || reference.isBlank())
                         ? "000-0000000000" : reference.trim();
        this.available = available;
        materialsCreated++;
    }

    // ---------- Parameters each format redefines ----------

    /** @return term in days for this type of material. */
    public int getLoanDays() {
        return LOAN_DAYS;
    }

    /** @return cost per day late for this type of material. */
    public double getDailyRate() {
        return DAILY_RATE;
    }

    /** @return readable name of the format, for listings. */
    public String getType() {
        return "Material";
    }

    // ---------- Common rules, written ONCE ----------

    /** @return days late, clamped to zero, according to this material's term. */
    public int calculateDaysLate(int elapsedDays) {
        return Math.max(0, elapsedDays - getLoanDays());
    }

    /** @return fine in euros, with the common cap applied. */
    public double calculateFine(int elapsedDays) {
        int late = calculateDaysLate(elapsedDays);
        return Math.min(late * getDailyRate(), MAX_FINE);
    }

    /** @return "ON TIME", "MINOR" or "SEVERE". */
    public String classifySeverity(int elapsedDays) {
        int late = calculateDaysLate(elapsedDays);
        if (late == 0)                return "ON TIME";
        if (late <= MINOR_THRESHOLD)  return "MINOR";
        return "SEVERE";
    }

    public boolean isAvailable() { return available; }

    public void lend() {
        if (!available) {
            System.out.println("WARNING: '" + title + "' was already on loan.");
            return;
        }
        available = false;
    }

    public void returnItem() {
        if (available) {
            System.out.println("WARNING: '" + title + "' was already available.");
            return;
        }
        available = true;
    }

    /** @return readable description; subclasses extend it with super.describe(). */
    public String describe() {
        return title + " (ref. " + reference + ")";
    }
}

Note the key piece of the design: calculateFine does not use the constants directly, it calls getLoanDays() and getDailyRate(). Since those methods can be overridden, the formula is written once and works for every format. It is the mechanism the next lesson will formalise under the name dynamic dispatch.

The three subclasses

package com.nexussoftware.bibliotech.domain;

/** Technical book from the catalog. It uses the standard term and rate. */
public class Book extends Material {

    public final String author;
    public final int    publicationYear;

    public Book(String title, String author, String isbn,
                int publicationYear, boolean available) {
        super(title, isbn, available);            // the ISBN is the book's reference
        this.author = (author == null || author.isBlank()) ? "Unknown" : author.trim();

        if (publicationYear < 1450 || publicationYear > 2100) {
            System.out.println("WARNING: invalid year in '" + title + "'. Recorded as 0.");
            this.publicationYear = 0;
        } else {
            this.publicationYear = publicationYear;
        }
    }

    /** Usual entry: a new book comes in available. */
    public Book(String title, String author, String isbn, int publicationYear) {
        this(title, author, isbn, publicationYear, true);
    }

    /** The ISBN is a book's reference; the domain name is kept. */
    public String getIsbn() { return reference; }

    @Override
    public String getType() { return "Book"; }

    @Override
    public String describe() {
        return super.describe() + " - " + author + ", " + publicationYear;
    }

    // It does not override getLoanDays() or getDailyRate():
    // it inherits the standard values of 15 days and EUR 0.25/day.
}
package com.nexussoftware.bibliotech.domain;

/** Technical magazine. It circulates a lot, so its term is shorter and its rate lower. */
public class Magazine extends Material {

    public static final int    MAGAZINE_LOAN_DAYS  = 7;
    public static final double MAGAZINE_DAILY_RATE = 0.10;

    public final int    number;
    public final String frequency;

    public Magazine(String title, String reference, int number, String frequency) {
        super(title, reference, true);
        this.number    = Math.max(0, number);
        this.frequency = (frequency == null || frequency.isBlank())
                         ? "Unknown" : frequency.trim();
    }

    @Override
    public int getLoanDays() { return MAGAZINE_LOAN_DAYS; }

    @Override
    public double getDailyRate() { return MAGAZINE_DAILY_RATE; }

    @Override
    public String getType() { return "Magazine"; }

    @Override
    public String describe() {
        return super.describe() + " - no." + number + " (" + frequency + ")";
    }
}
package com.nexussoftware.bibliotech.domain;

/** Training DVD. Few copies: very short term and double rate. */
public class Dvd extends Material {

    public static final int    DVD_LOAN_DAYS  = 3;
    public static final double DVD_DAILY_RATE = 0.50;

    public final int durationMinutes;

    public Dvd(String title, String reference, int durationMinutes) {
        super(title, reference, true);
        this.durationMinutes = Math.max(0, durationMinutes);
    }

    @Override
    public int getLoanDays() { return DVD_LOAN_DAYS; }

    @Override
    public double getDailyRate() { return DVD_DAILY_RATE; }

    @Override
    public String getType() { return "DVD"; }

    @Override
    public String describe() {
        return super.describe() + " - " + durationMinutes + " min";
    }
}

Comparison of the three formats

Format Term Rate/day Fine at 10 elapsed days Own fields
Book 15 days €0.25 €0.00 (still within term) author, publicationYear
Magazine 7 days €0.10 €0.30 (3 days late) number, frequency
Dvd 3 days €0.50 €3.50 (7 days late) durationMinutes

Test in BiblioTechApp

Book     effectiveJava = new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018);
Magazine javaMagazine  = new Magazine("Java Magazine", "REV-2024-42", 42, "Bimonthly");
Dvd      springCourse  = new Dvd("Spring Course", "DVD-0007", 240);

System.out.printf("%-9s %-22s %6s %8s %10s %10s%n",
                  "TYPE", "TITLE", "TERM", "RATE", "FINE 10d", "SEVERITY");

printCard(effectiveJava);
printCard(javaMagazine);
printCard(springCourse);

System.out.println();
System.out.println(effectiveJava.describe());
System.out.println(javaMagazine.describe());
System.out.println(springCourse.describe());
private static void printCard(Material m) {
    System.out.printf("%-9s %-22s %4d d %7.2f %9.2f  %s%n",
                      m.getType(), m.title, m.getLoanDays(), m.getDailyRate(),
                      m.calculateFine(10), m.classifySeverity(10));
}

Output:

TYPE      TITLE                    TERM     RATE   FINE 10d   SEVERITY
Book      Effective Java           15 d     0.25       0.00  ON TIME
Magazine  Java Magazine              7 d     0.10       0.30  MINOR
DVD       Spring Course              3 d     0.50       3.50  MINOR

Effective Java (ref. 978-0000000001) - Joshua Bloch, 2018
Java Magazine (ref. REV-2024-42) - no.42 (Bimonthly)
Spring Course (ref. DVD-0007) - 240 min

Notice something that may have slipped past you: the printCard method receives a parameter of type Material and we pass it a Book, a Magazine and a Dvd. It works, and each one answers according to its own type. That is polymorphism, the subject of the next lesson.

Note on the project's evolution. The Material class as written can be instantiated (new Material(...)), and that makes no sense: in the library there are no "generic materials", there are books, magazines and DVDs. In lesson 04-02 you will turn Material into an abstract class, which prevents loose instances and lets you declare bodiless operations that subclasses are obliged to implement. For now, consider Material a working scaffold.

Common Mistakes and Tips

  • Forgetting @Override. It is the mistake that produces the hardest bugs to find: a misspelled method overrides nothing and the superclass keeps ruling. Always put @Override.
  • Believing the subclass can access the parent's private fields. They exist in the object, but they are not accessible. If the subclass needs them, the superclass must expose a method (or declare them protected, carefully; see 03-07).
  • Expecting constructors to be inherited. They are not. Each subclass declares its own and calls super(...).
  • Using inheritance to reuse code, with no "is a" relationship. The Loan extends Book case from section 11. If you only want to reuse, compose.
  • Calling overridable methods from the constructor. The field is still null. Documented in section 6.
  • Making visibility less permissive when overriding. public → protected does not compile: it would break the superclass's contract.
  • Hierarchies five or six levels deep. They cost more to understand than the problem they solve.
  • Tip: say the sentence out loud. "A DVD is a lendable material": sounds right, inheritance. "A loan is a book": sounds wrong, composition.
  • Tip: move up to the superclass only what is common to all the subclasses. If a field is used by two out of three, it does not belong in the base class.
  • Tip: use the IDE's hierarchy view (Ctrl+H in IntelliJ) to see at a glance who extends whom and which methods are overridden.

Exercises

Exercise 1: add a new format

Nexus Software is adding audiobooks to the catalog: they are lent for 10 days, with a rate of €0.15/day, and they have their own fields narrator and durationMinutes. Create the AudioBook class as a subclass of Material, overriding whatever is needed and without touching a single line of Material, Book, Magazine or Dvd. Add it to BiblioTechApp's comparison table and check that the fine at 20 elapsed days is €1.50.

Exercise 2: trace the order of construction

Write a three-level hierarchy (Material → Book → SignedBook) where each class has an instance initialisation block and a constructor, both with an identifying System.out.println. Predict the output of new SignedBook(...) before running it and then verify it. Next, deliberately trigger the problem from section 6: make Material's constructor call a method that SignedBook overrides using one of its fields, and explain the result.

Exercise 3: spot badly applied inheritance

A colleague proposes these four hierarchies for BiblioTech. For each one, decide whether inheritance is appropriate and, if it is not, propose the correct alternative with composition:

class Employee extends Material { }
class Librarian extends Employee { }
class Catalog extends Book { }
class DiscontinuedBook extends Book { }

Solutions

Solution 1

package com.nexussoftware.bibliotech.domain;

/** Audiobook from the catalog. Intermediate term and reduced rate. */
public class AudioBook extends Material {

    public static final int    AUDIO_LOAN_DAYS  = 10;
    public static final double AUDIO_DAILY_RATE = 0.15;

    public final String narrator;
    public final int    durationMinutes;

    public AudioBook(String title, String reference,
                     String narrator, int durationMinutes) {
        super(title, reference, true);
        this.narrator = (narrator == null || narrator.isBlank())
                        ? "Unknown" : narrator.trim();
        this.durationMinutes = Math.max(0, durationMinutes);
    }

    @Override
    public int getLoanDays() { return AUDIO_LOAN_DAYS; }

    @Override
    public double getDailyRate() { return AUDIO_DAILY_RATE; }

    @Override
    public String getType() { return "Audiobook"; }

    @Override
    public String describe() {
        return super.describe() + " - narrated by " + narrator
               + ", " + durationMinutes + " min";
    }
}

Use:

AudioBook audio = new AudioBook("Refactoring", "AUD-0003", "Nuria Vidal", 610);
printCard(audio);
System.out.println(audio.describe());
System.out.printf("Fine at 20 days: %.2f EUR (%s)%n",
                  audio.calculateFine(20), audio.classifySeverity(20));

Output:

Audiobook Refactoring              10 d     0.15       0.00  ON TIME
Refactoring (ref. AUD-0003) - narrated by Nuria Vidal, 610 min
Fine at 20 days: 1.50 EUR (SEVERE)

Check both calculations by hand, because it is the best way to verify that inheritance behaves as you expect. With 10 elapsed days (the value printCard uses), the audiobook's term is 10 days, so the delay is 0: there is no fine and the severity is ON TIME. With 20 elapsed days, the delay is 20 − 10 = 10 days; 10 × 0.15 = €1.50, well below the €20 cap; and since 10 exceeds the MINOR_THRESHOLD of 7 days, the severity is SEVERE.

The important part of the exercise: to add a new format you have written one class and zero modifications to the existing code. printCard works with audiobooks without ever finding out they exist. That property —extend without modifying— is one of the reasons OOP took over large-scale software, and the next lesson exploits it thoroughly.

Solution 2

package com.nexussoftware.bibliotech;

class MaterialTrace {
    { System.out.println("1. Material block"); }

    MaterialTrace() {
        System.out.println("2. Material constructor");
        System.out.println("   describe() -> " + describe());     // DANGER
    }

    String describe() { return "generic material"; }
}

class BookTrace extends MaterialTrace {
    { System.out.println("3. Book block"); }

    BookTrace() {
        super();
        System.out.println("4. Book constructor");
    }
}

class SignedBookTrace extends BookTrace {
    private final String signer;

    { System.out.println("5. SignedBook block"); }

    SignedBookTrace(String signer) {
        super();
        this.signer = signer;
        System.out.println("6. SignedBook constructor");
    }

    @Override
    String describe() { return "book signed by " + signer; }
}
new SignedBookTrace("Martin Fowler");

Output:

1. Material block
2. Material constructor
   describe() -> book signed by null
3. Book block
4. Book constructor
5. SignedBook block
6. SignedBook constructor

Explanation. Construction goes from the root to the leaf: each level's blocks and constructor run completely before moving on to the next. That is why the order is 1-2, 3-4, 5-6.

The problem from section 6 shows up in the third line. MaterialTrace's constructor calls describe(), and since the actual object is a SignedBookTrace, the JVM runs the overridden version (this is the dynamic dispatch of the next lesson). But at that instant the signer field has not been assigned yet —its assignment is in step 6, four steps later—, so it is null and the output is book signed by null. And notice the unsettling detail: signer is final, a field that "cannot change", and yet we have observed it with two different values during the object's life.

Solution 3

class Employee extends Material — WRONG. "An employee is a lendable material" is false and, on top of that, morally questionable. Employee would inherit lend(), returnItem() and calculateFine(), all meaningless, and could end up in a catalog of materials. Alternative: no relationship at all. Employee is an independent class that appears associated from Loan.

class Librarian extends Employee — CORRECT (with nuances). "A librarian is an employee" is true, and a librarian would have behaviour of their own (registering new materials, cancelling fines) on top of the inherited behaviour. It is a legitimate use. The nuance: if the only difference were a String role field, inheritance would be overkill; a field would be enough. Introduce the subclass only when it contributes different behaviour, not just a piece of data.

class Catalog extends Book — WRONG, and one of the worst. A catalog is not a book: a catalog contains books. Here the container is confused with the content, a classic mistake. Catalog would inherit lend(), getIsbn() and author, all absurd for a collection. The alternative is composition, which is exactly what you will do in module 5:

public class Catalog {
    // It contains materials; multiple storage arrives in module 5.
    // For now it would be an array; later, an ArrayList or a HashMap.
}

class DiscontinuedBook extends Book — DOUBTFUL. The sentence "a discontinued book is a book" is true, so inheritance is not absurd. But ask yourself what behaviour changes: probably only that it cannot be lent. That is a state, not a type:

public class Book extends Material {
    private boolean discontinued;

    @Override
    public void lend() {
        if (discontinued) {
            System.out.println("WARNING: '" + title + "' is discontinued.");
            return;
        }
        super.lend();
    }
}

The general warning sign: if an object can move from one subclass to another during its life —a book is discontinued and then reinstated—, inheritance is the wrong mechanism, because in Java an object cannot change class once created. States that change are modelled with fields.

Conclusion

You have learned the mechanism with which Java expresses specialisation. You know when it is appropriate —only when the sentence "X is a Y" is always true— and how it is written with extends. You know precisely what is inherited and what is not: private fields exist but are not accessible, and constructors are never inherited. You master super(...) for chaining constructors and super.method() for extending behaviour without duplicating it, and you have traced the order of construction of a full hierarchy, including the real danger of calling overridable methods from a constructor and finding final fields holding null. You are clear —and backed by a table— about the difference between overriding and overloading, and you know that @Override is not decoration but the safety net that turns a silent typo into a compile error. You know how to use final to prohibit extension, you know the universal root Object and, above all, you have the judgement to prefer composition over inheritance and to recognise deep hierarchies and the fragile base class before suffering them.

BiblioTech has changed scale: it no longer manages books, it manages lendable materials. Material concentrates identification, availability and the fine rules written only once, and Book, Magazine and Dvd declare only how they differ: their term and their rate. Adding an audiobook cost you one new class and zero changes to the existing code.

And something has appeared that does not have a name yet. printCard(Material m) receives a book, a magazine or a DVD indistinctly, and each one answers according to what it really is. calculateFine is written only once, in Material, and yet it charges €0.25 for the book and €0.50 for the DVD. That mechanism is called polymorphism, it is the pillar that makes inheritance worth it, and it is the subject of the next lesson: you will see how the JVM decides at run time which method to execute, what upcasting and downcasting are, how modern Java's instanceof with patterns replaces the old ifs on the type, and why those ifs were, from the very start, a design smell.

Java Programming Course

Module 1: Introduction to Java

Module 2: Control Flow

Module 3: Object-Oriented Programming

Module 4: Advanced Object-Oriented Programming

Module 5: Data Structures and Collections

Module 6: Exception Handling

Module 7: File Input/Output

Module 8: Multithreading and Concurrency

Module 9: Networking

Module 10: Advanced Topics

Module 11: Java Frameworks and Libraries

Module 12: Building Real-World Applications

© Copyright 2026. All rights reserved