When the previous lesson closed, a crack was left open in BiblioTech: Material is still an instantiable class. Nothing stops you writing new Material("Something", "REF-1", true) and getting an object that is not a book, nor a magazine, nor a DVD; an object with no author, no number and no duration, which answers "Material" when you ask for its type. It is an object that should not exist, and its existence is an accident of the design, not a decision.

The abstract class closes that door. It is a class that explicitly declares itself incomplete: it serves as a common base — with fields, constructor and shared code — but it cannot be instantiated, and it can force its subclasses to implement the methods only they can decide. Where the interface says "this is what you must know how to do", the abstract class says "this is what is already done, and this is your part". By the end of this lesson, Material will be abstract, calculateFine will be written once only for the whole system through the Template Method pattern, and you will be able to choose with judgement between interface, abstract class or — the most frequent option in professional design — both at once.

Contents

  1. abstract on classes: what it means and what it prevents
  2. Abstract methods: the obligation that is inherited
  3. An abstract class WITH state: fields, constructor and concrete methods
  4. What is the constructor of a class that is never instantiated for?
  5. An incomplete subclass must also be abstract
  6. Interface versus abstract class: the full table
  7. A practical decision rule
  8. The Template Method pattern
  9. Combining the two: AbstractXxx
  10. BiblioTech: Material becomes abstract
  11. Refactoring Book, Magazine and Dvd
  12. Common Mistakes and Tips
  13. Exercises

  1. abstract on classes: what it means and what it prevents

The abstract modifier applied to a class declares that the class is conceptually incomplete: it represents a general idea of which no concrete specimens exist.

public abstract class Material {
    // ... exactly the same as before ...
}

With that single word, this stops compiling:

Material m = new Material("Something", "REF-1", true);
error: Material is abstract; cannot be instantiated

It is important to understand what is forbidden and what is not:

Operation Allowed in an abstract class?
new Material(...) No. Compilation error
Material m = new Book(...) Yes. It is a valid type to declare
Material[] catalog = new Material[10] Yes. The array stores references, not instances
Having fields, even private ones Yes
Having a constructor Yes, and it is essential (section 4)
Having concrete methods with a body Yes
Having abstract methods without a body Yes, and that is the characteristic feature
Having static and final methods Yes
Extending another class Yes
Implementing interfaces Yes, and it is very common (section 9)
Having no abstract method at all Yes, it is legal (though uncommon)

That last point is surprising: a class can be abstract without a single abstract method. It is perfectly valid and sometimes useful: you mark it abstract simply to declare that instantiating it makes no sense, even though it is technically complete.

The immediate gain is in modelling. "Material" is an abstraction: in the Nexus Software library there are books, magazines and DVDs, but there are no plain "materials". The code starts telling the truth about the domain, and the compiler enforces it.

  1. Abstract methods: the obligation that is inherited

An abstract method is a method declared without a body, ending in a semicolon, which the class promises will exist but refuses to implement:

public abstract class Material {

    /** Each format declares its readable label. */
    public abstract String getType();

    /** Each format declares its loan term in days. */
    public abstract int getLoanDays();

    /** Each format declares its daily fine rate. */
    public abstract double getDailyRate();
}

The rules are strict and it is worth having them all together:

  • An abstract method can only exist inside an abstract class (or an interface). If you put an abstract method in a normal class, the error is missing method body, or declare abstract.
  • Every concrete subclass must implement them all. If one is missing, it does not compile.
  • abstract is incompatible with private, static and final. With private because the subclass would not see it; with static because static methods are not polymorphic (03-06); with final because final forbids exactly what abstract demands.

The difference from the previous design is one of guarantees. Compare the two versions of getLoanDays:

// BEFORE (module 3): filler implementation
public int getLoanDays() { return LOAN_DAYS; }   // 15 days "just in case"

// NOW: an obligation
public abstract int getLoanDays();

With the first, if tomorrow you add AudioBook extends Material and forget its term, the program compiles and silently applies 15 days. The error is not detected until an employee complains about a miscalculated fine. With the second, it does not compile: the error arrives at second zero, with the method name and the line number.

This is the difference between a default value and a contract. The default value hides the oversight; the contract reports it.

  1. An abstract class WITH state: fields, constructor and concrete methods

Here is the essential difference from an interface: an abstract class can store data. And therefore it can write genuinely shared code, not just code that leans on calls to the contract.

public abstract class Material {

    // 1. Shared constants
    public static final double MAX_FINE        = 20.0;
    public static final int    MINOR_THRESHOLD = 7;

    // 2. Instance state: IMPOSSIBLE in an interface
    private final String title;
    private final String reference;
    private boolean      available;

    // 3. Constructor: IMPOSSIBLE in an interface
    protected 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;
    }

    // 4. Concrete methods operating on that state
    public String  getTitle()     { return title; }
    public String  getReference() { return reference; }
    public boolean isAvailable()  { return available; }

    public boolean lend() {
        if (!available) { return false; }
        available = false;
        return true;
    }

    // 5. Abstract methods the subclasses must fill in
    public abstract String getType();
    public abstract int    getLoanDays();
    public abstract double getDailyRate();
}

The five numbered sections sum up the split: the abstract class provides what is common (2, 3, 4), and delegates what varies (5). No interface can offer points 2 and 3.

It is the direct answer to a question you may have asked yourself in 04-01: if a default method can have a body, why do I need an abstract class? Because lend() needs the available field. A default cannot have it: it would have to call isAvailable() and a hypothetical setAvailable(), widening the contract with a mutator that breaks the encapsulation you looked after so carefully in 03-07. The abstract class stores the field, keeps it private and exposes only the business operations.

  1. What is the constructor of an abstract class for?

It is the classic doubt: if new Material(...) is forbidden, what is the point of that constructor?

The answer lies in the initialisation chain you studied in 03-04: a subclass constructor always invokes its superclass's constructor first, explicitly with super(...) or implicitly. That constructor does run, it simply never runs on its own.

public class Book extends Material {
    public Book(String title, String author, String isbn, int year) {
        super(title, isbn, true);         // runs the Material constructor
        this.author = author;
        this.publicationYear = year;
    }
}
flowchart TD
    A["new Book(...)"] --> B["memory is reserved for the whole object"]
    B --> C["Book constructor: super(title, isbn, true)"]
    C --> D["Object constructor"]
    D --> E["body of the Material constructor: validates and initialises title, reference, available"]
    E --> F["body of the Book constructor: initialises author and publicationYear"]
    F --> G["fully constructed Book object"]

So the constructor of an abstract class has three very concrete jobs:

  1. Initialising the common state the subclass must not touch (the fields are private).
  2. Centralising validation. The title and reference checks are written once and applied by Book, Magazine and Dvd, with no way to skip them.
  3. Guaranteeing the invariants of the common part from the object's first instant of life.

And there is a design decision in the signature: declaring it protected instead of public. In an abstract class, public on the constructor is misleading — it suggests someone can call it from outside, and they cannot — whereas protected says exactly the truth: this constructor exists for the subclasses. It is the convention the JDK itself follows.

  1. An incomplete subclass must also be abstract

What happens if a subclass implements only some of the abstract methods? It remains incomplete, and Java forces you to say so:

/** Common base for formats consulted on site that never leave the building. */
public abstract class ReferenceMaterial extends Material {

    protected ReferenceMaterial(String title, String reference) {
        super(title, reference, true);
    }

    /** All reference materials share term and rate... */
    @Override public int    getLoanDays()  { return 1; }
    @Override public double getDailyRate() { return 1.0; }

    // ... but getType() is still unimplemented: the class is still abstract
}

If you removed the abstract from ReferenceMaterial, the compiler would say:

error: ReferenceMaterial is not abstract and does not override
       abstract method getType() in Material

The obligation is inherited downwards until somebody fulfils it. This allows multi-level hierarchies where each level solves what it knows and leaves the rest pending:

classDiagram
    class Material {
        <<abstract>>
        +getType()* String
        +getLoanDays()* int
        +getDailyRate()* double
    }
    class ReferenceMaterial {
        <<abstract>>
        +getLoanDays() int
        +getDailyRate() double
    }
    class Atlas {
        +getType() String
    }
    Material <|-- ReferenceMaterial
    ReferenceMaterial <|-- Atlas

Atlas is the first concrete class in the branch: it implements the only thing still pending and can now be instantiated.

  1. Interface versus abstract class: the full table

This is the table announced in 04-01. Study it in full: it covers the six dimensions that really decide.

Dimension Interface Abstract class
Instance state Impossible. Only public static final Fields of any type and visibility
Constructor It has none Yes, and it runs via super(...)
Multiple inheritance A class implements several A class extends only one
Member visibility Everything public (except Java 9's private, invisible outside) public, protected, package, private
Methods with a body Yes: default, static, private Yes, with no restrictions
Initialisation blocks No Yes, static and instance ones
protected members No Yes: the natural mechanism for subclasses
API evolution Adding an abstract method breaks; adding a default does not Adding a concrete method does not break; an abstract one does
Relationship it expresses "can do" (capability) "is a" (identity)
Coupling it imposes Minimal: it does not consume the inheritance slot High: it consumes the only inheritance available
JDK example Comparable, Runnable, List AbstractList, InputStream, Number

Three rows deserve extra comment:

API evolution. It is asymmetric and often confused. In an interface, adding an abstract method breaks all implementers, but a default breaks nobody. In an abstract class, adding a concrete method is safe (all subclasses inherit it), but adding an abstract one breaks all concrete subclasses. Each has its own safe way to grow.

Coupling. It is the decisive argument and often the forgotten one. If you force your users to extend your abstract class, you spend their only inheritance slot and they will not be able to extend anything else. An interface costs nothing in that respect. That is why the professional recommendation is: the public type is the interface; the abstract class is optional help.

protected members. An abstract class can offer its subclasses tools the rest of the world does not see. An interface cannot: everything it declares is public. When you need that "private channel" towards the subclasses, the abstract class is the only option.

  1. A practical decision rule

flowchart TD
    A["I need a common type"] --> B{"Is shared state or a constructor needed?"}
    B -- "No" --> C{"Will unrelated classes sign it?"}
    B -- "Yes" --> D["Abstract class"]
    C -- "Yes" --> E["Interface"]
    C -- "No" --> F{"Do I need shared code?"}
    F -- "No" --> E
    F -- "Yes" --> G["Interface plus abstract base class"]
    D --> H{"Do I also want others to sign it?"}
    H -- "Yes" --> G
    H -- "No" --> D

And in three memorable sentences:

  • Interface when you define what can be done and you want classes that share no family to sign it. It is the default option: always start here.
  • Abstract class when you share state, constructor and real code between classes that do form a genuine family.
  • Both when you want the two things: the interface as the public type and the abstract class as an optional base implementation. It is what the JDK does and what you will do in BiblioTech.

  1. The Template Method pattern

Now comes one of the most powerful uses of abstract classes. Look at this method, which has existed in your Material since module 3:

public double calculateFine(int elapsedDays) {
    return Math.min(calculateDaysLate(elapsedDays) * getDailyRate(),
                    MAX_FINE);
}

The interesting thing is its structure: it defines the invariable skeleton of the algorithm — compute the delay, multiply it by the rate, apply the cap — but delegates to getDailyRate(), which each subclass implements its own way. The skeleton is written once; the variable steps, as many times as there are formats.

That is exactly the Template Method pattern: a concrete method in the base class defines the sequence of steps, and the steps that vary are abstract methods the subclasses fill in.

And there is a key detail that makes the pattern solid: the template method is declared final.

/**
 * TEMPLATE: the sequence of the calculation is company policy
 * and no subclass may alter it.
 */
public final double calculateFine(int elapsedDays) {
    int daysLate = calculateDaysLate(elapsedDays);      // fixed step
    double gross = daysLate * getDailyRate();           // VARIABLE STEP
    return Math.min(gross, MAX_FINE);                   // fixed step (cap)
}

Why final? Because the €20 cap and the formula are Nexus Software policy, not a decision for each format. Without final, a Dvd could override calculateFine and skip the cap. With final, the compiler prevents it: subclasses can only influence the result through the gaps the template offers them.

flowchart TD
    A["calculateFine(days) FINAL in Material"] --> B["fixed step: calculateDaysLate(days)"]
    B --> C["VARIABLE STEP: getDailyRate()"]
    C --> D["fixed step: apply MAX_FINE"]
    D --> E["result"]
    C -.-> F["Book returns 0.25"]
    C -.-> G["Magazine returns 0.10"]
    C -.-> H["Dvd returns 0.50"]

Splitting responsibilities this way has a name in the literature: the Hollywood principle — "don't call us, we'll call you". The subclass does not control the flow; it merely fills in the gaps the base class reserves for it.

This pattern is formally named because it is one of the classic design patterns. Here you have discovered it naturally, by writing the class that was needed; in lesson 12-02 you will see it formalised alongside Strategy, Factory, Observer and the rest, with its full description and its alternatives.

  1. Combining the two: AbstractXxx

You almost never choose between interface and abstract class: you use both, in layers.

  • The interface is the public type. What variables, parameters and return values declare.
  • The abstract class implements the interface and offers a partial base with the repetitive work already solved.
  • The concrete classes extend the base and only write their own part.

And whoever does not want the base implements the interface directly. The help is optional, and that is the whole point.

The JDK is built this way, and the naming convention gives it away:

Interface (public type) Partial base (optional help) Concrete class
List AbstractList ArrayList, LinkedList
Map AbstractMap HashMap, TreeMap
Set AbstractSet HashSet

ArrayList is an AbstractList and is a List. But you can write your own List without touching AbstractList. All these classes arrive in module 5; what matters today is recognising the structural pattern.

In BiblioTech the split works out exactly the same:

classDiagram
    class Lendable {
        <<interface>>
        +lend() boolean
        +returnItem() boolean
        +isAvailable() boolean
        +getLoanDays() int
    }
    class Notifiable {
        <<interface>>
        +getNoticeChannel() String
        +buildNotice(int) String
    }
    class Material {
        <<abstract>>
        -title String
        -reference String
        -available boolean
        +calculateFine(int)$ double
        +getType()* String
    }
    class MeetingRoom
    Lendable <|.. Material
    Notifiable <|.. Material
    Lendable <|.. MeetingRoom
    Material <|-- Book
    Material <|-- Magazine
    Material <|-- Dvd

MeetingRoom implements Lendable without going through Material: it does not want the base, and it does not need it. It is the proof that the help is optional.

  1. BiblioTech: Material becomes abstract

This is the final state of the class after applying everything you have learned.

package com.nexussoftware.bibliotech.domain;

/**
 * Abstract base of every material in the Nexus Software catalogue.
 *
 * <p>It defines the common state (title, reference, availability), the shared
 * business rules and the template for the fine calculation. Each concrete
 * format provides its type, its term and its rate.</p>
 */
public abstract class Material implements Lendable, Notifiable {

    public static final double MAX_FINE        = 20.0;
    public static final int    MINOR_THRESHOLD = 7;

    private static int materialsCreated = 0;

    private final String title;
    private final String reference;
    private boolean      available;

    /** Protected constructor: it exists for the subclasses, not for the outside. */
    protected 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++;
    }

    // ---------- What EACH format must declare ----------

    /** @return readable label of the format: "Book", "Magazine", "DVD". */
    public abstract String getType();

    /** @return loan term of this format, in days. */
    @Override public abstract int getLoanDays();

    /** @return euros of fine per day late for this format. */
    public abstract double getDailyRate();

    // ---------- Common state (implemented once only) ----------

    public String  getTitle()     { return title; }
    public String  getReference() { return reference; }
    @Override public boolean isAvailable() { return available; }

    public static int getMaterialsCreated() { return materialsCreated; }

    // ---------- Business rules: final TEMPLATES ----------

    /** Days late beyond the format's term. Never negative. */
    public final int calculateDaysLate(int elapsedDays) {
        return Math.max(0, elapsedDays - getLoanDays());
    }

    /**
     * TEMPLATE METHOD. The skeleton of the calculation is company policy
     * and that is why it is final; the variable step is getDailyRate().
     */
    public final double calculateFine(int elapsedDays) {
        int    daysLate = calculateDaysLate(elapsedDays);
        double gross    = daysLate * getDailyRate();
        return Math.min(gross, MAX_FINE);
    }

    /** Delay classification. In 04-07 it will stop returning a String. */
    public final String classifySeverity(int elapsedDays) {
        int daysLate = calculateDaysLate(elapsedDays);
        if (daysLate == 0)               { return "ON TIME"; }
        if (daysLate <= MINOR_THRESHOLD) { return "MINOR"; }
        return "SEVERE";
    }

    // ---------- Business operations (Lendable contract) ----------

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

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

    // ---------- Notifiable contract ----------

    @Override public String getNoticeChannel() { return "email"; }

    public int getNoticeDays() { return 2; }

    @Override
    public String buildNotice(int elapsedDays) {
        return String.format("Notice by %s with %d days' warning: '%s' has run up %.2f EUR.",
                             getNoticeChannel(), getNoticeDays(), title,
                             calculateFine(elapsedDays));
    }

    // ---------- Representation and identity ----------

    public String describe() {
        return getType() + " \"" + title + "\" (ref. " + reference + ")";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) { return true; }
        if (o == null || getClass() != o.getClass()) { return false; }
        Material other = (Material) o;
        return reference.equals(other.reference);
    }

    @Override
    public int hashCode() { return reference.hashCode(); }

    @Override
    public String toString() {
        return getType() + "[ref=" + reference + ", title=" + title
             + ", available=" + available + "]";
    }
}

Four changes that deserve attention:

  1. abstract class: new Material(...) no longer compiles.
  2. protected constructor: it tells the truth about who can call it.
  3. getType(), getLoanDays() and getDailyRate() are abstract: the filler constants LOAN_DAYS = 15 and DAILY_RATE = 0.25 disappear from Material. Each format declares its own and nobody can forget them.
  4. calculateDaysLate, calculateFine and classifySeverity are final: they are company policy, not negotiable per subclass.

And notice describe(): it now calls getType(), an abstract method. A concrete method of the base class invoking a method that does not exist yet. It works thanks to the dynamic dispatch of 03-06: at run time, this is always a concrete object, and its getType() is implemented.

  1. Refactoring Book, Magazine and Dvd

Now each format declares its three decisions, and none can be omitted.

package com.nexussoftware.bibliotech.domain;

/** Technical book in the catalogue. Long term and standard rate. */
public class Book extends Material {

    public static final int    BOOK_LOAN_DAYS  = 15;
    public static final double BOOK_DAILY_RATE = 0.25;

    private final String author;
    private final int    publicationYear;

    public Book(String title, String author, String isbn,
                int publicationYear, boolean available) {
        super(title, isbn, available);
        this.author = (author == null || author.isBlank()) ? "Unknown" : author.trim();
        this.publicationYear = (publicationYear < 1450 || publicationYear > 2100)
                               ? 0 : publicationYear;
    }

    public Book(String title, String author, String isbn, int publicationYear) {
        this(title, author, isbn, publicationYear, true);
    }

    public String getAuthor()          { return author; }
    public int    getPublicationYear() { return publicationYear; }
    public String getIsbn()            { return getReference(); }

    // --- The three MANDATORY decisions ---
    @Override public String getType()      { return "Book"; }
    @Override public int    getLoanDays()  { return BOOK_LOAN_DAYS; }
    @Override public double getDailyRate() { return BOOK_DAILY_RATE; }

    @Override
    public String describe() {
        return super.describe() + " - " + author + ", " + publicationYear;
    }
}
package com.nexussoftware.bibliotech.domain;

/** Technical magazine. It circulates a lot: short term and low rate. */
public class Magazine extends Material {

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

    private final int    number;
    private 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();
    }

    public int    getNumber()    { return number; }
    public String getFrequency() { return frequency; }

    @Override public String getType()      { return "Magazine"; }
    @Override public int    getLoanDays()  { return MAGAZINE_LOAN_DAYS; }
    @Override public double getDailyRate() { return MAGAZINE_DAILY_RATE; }

    @Override public String getNoticeChannel() { return "chat"; }
    @Override public int    getNoticeDays()    { return 1; }

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

/** Training DVD. Few copies: minimum 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;

    private final int durationMinutes;

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

    public int getDurationMinutes() { return durationMinutes; }

    @Override public String getType()      { return "DVD"; }
    @Override public int    getLoanDays()  { return DVD_LOAN_DAYS; }
    @Override public double getDailyRate() { return DVD_DAILY_RATE; }

    @Override public String getNoticeChannel() { return "phone"; }
    @Override public int    getNoticeDays()    { return 1; }

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

Testing the result:

Material[] catalog = {
    new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018),
    new Magazine("Java Magazine", "REV-2024-03", 42, "Monthly"),
    new Dvd("Refactoring Live", "DVD-0007", 95)
};

System.out.printf("%-10s %-6s %-8s %-10s %s%n",
                  "TYPE", "TERM", "RATE", "FINE@20d", "SEVERITY");
for (Material m : catalog) {
    System.out.printf("%-10s %-6d %-8.2f %-10.2f %s%n",
                      m.getType(), m.getLoanDays(), m.getDailyRate(),
                      m.calculateFine(20), m.classifySeverity(20));
}

// Material generic = new Material("X", "REF", true);   // DOES NOT COMPILE
TYPE       TERM   RATE     FINE@20d   SEVERITY
Book       15     0.25     1.25       MINOR
Magazine   7      0.10     1.30       SEVERE
DVD        3      0.50     8.50       SEVERE

A single calculateFine, written once only, producing three different and correct results. And the commented-out line is the main improvement: the meaningless object can no longer be created.

Common Mistakes and Tips

Putting an abstract method in a non-abstract class. The error is missing method body, or declare abstract. If a method cannot be implemented in the base class, the base class is abstract. There is no middle ground.

Trying new on the abstract class. Material m = new Material(...) does not compile. What is legal, and confuses many people, is Material m = new Book(...): the abstract class is a perfectly valid type to declare.

Declaring the constructor public in an abstract class. It compiles, but it lies: it suggests an access that does not exist. Use protected.

Combining abstract with final, static or private. All three combinations are contradictory and the compiler rejects them. abstract means "it must be overridden"; final means "it cannot be", static means "it is not polymorphic" and private means "it is not visible".

Forgetting that a partial subclass must remain abstract. If you implement two of three abstract methods, the resulting class is still incomplete.

Choosing an abstract class out of habit. It is the most expensive design trap in the module. By forcing extends, you spend your users' only inheritance slot. Always start with the interface and add the abstract base only if there is state or real code to share.

Calling an abstract (or overridable) method from the base constructor. It is a subtle and serious mistake: when the Material constructor runs, Book's fields are not yet initialised. If Material called in its constructor a describe() that uses author, you would get null. Rule: from a constructor, invoke only private, static or final methods.

Tip: final on the template method. That is what turns a hierarchy into a real Template Method. If the skeleton is not final, any subclass can bypass the policy.

Tip: document the contract of every abstract method. The Javadoc of an abstract method is the only place where you can tell whoever implements it what is expected: what it must return, what ranges are valid, whether it may return null. It is design by contract (03-08) applied at the exact point where it is needed most.

Exercises

Exercise 1: AudioBook

Add to BiblioTech a format AudioBook extends Material with its own field durationHours (double) and a method getNarrator(). Term: 10 days. Rate: €0.15/day. Notice channel: "email" (the inherited one). Check that the compiler forces you to implement the three abstract methods, and integrate the new format into the catalog array without touching the loop that walks it.

Exercise 2: Template Method for the receipt

Create an abstract class MaterialReport in the presentation package with:

  • A method public final String generate(Material m, int elapsedDays) returning header() + body(m, days) + footer().
  • header() and footer() as concrete methods with a default implementation.
  • body(Material, int) as an abstract method.

Create two subclasses: BriefReport (one line with title and fine) and DetailedReport (title, type, term, delay, fine and severity, one per line). Check that the skeleton is written once only.

Exercise 3: choosing interface or abstract class

For each case, decide whether you would use an interface, an abstract class or both, and justify it in one sentence:

  1. Every BiblioTech object that can be exported to a text file.
  2. The common base of three employee types (Internal, External, Intern) that share name, identifier and the loan limit, but with a different maximum.
  3. The ability to compare two materials in order to sort them.
  4. A loan catalogue with different future implementations: in memory, in a file and in a database.

Solutions

Solution 1

package com.nexussoftware.bibliotech.domain;

/** Audiobook in the catalogue. 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;

    private final String narrator;
    private final double durationHours;

    public AudioBook(String title, String reference,
                     String narrator, double durationHours) {
        super(title, reference, true);            // validation centralised in Material
        this.narrator      = (narrator == null || narrator.isBlank())
                             ? "Unknown" : narrator.trim();
        this.durationHours = Math.max(0.0, durationHours);
    }

    public String getNarrator()      { return narrator; }
    public double getDurationHours() { return durationHours; }

    // The three methods the compiler DEMANDS:
    @Override public String getType()      { return "Audiobook"; }
    @Override public int    getLoanDays()  { return AUDIO_LOAN_DAYS; }
    @Override public double getDailyRate() { return AUDIO_DAILY_RATE; }

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

If you leave out, for example, getDailyRate():

error: AudioBook is not abstract and does not override
       abstract method getDailyRate() in Material

And the integration:

Material[] catalog = {
    new Book("Effective Java", "Joshua Bloch", "978-0000000001", 2018),
    new Magazine("Java Magazine", "REV-2024-03", 42, "Monthly"),
    new Dvd("Refactoring Live", "DVD-0007", 95),
    new AudioBook("Design Patterns", "AUD-0002", "Nuria Vidal", 12.5)   // the only new line
};
TYPE       TERM   RATE     FINE@20d   SEVERITY
Book       15     0.25     1.25       MINOR
Magazine   7      0.10     1.30       SEVERE
DVD        3      0.50     8.50       SEVERE
Audiobook  10     0.15     1.50       SEVERE

The loop has not been touched. A new format costs one class and one line of data: zero modifications to the existing logic. And what is new compared with module 3 is that the compiler has guaranteed that you declared term, rate and type. With the old version of Material, forgetting the rate would have silently charged €0.25/day.

Solution 2

package com.nexussoftware.bibliotech.presentation;

import com.nexussoftware.bibliotech.domain.Material;

/** Base of every material report. It defines the skeleton, not the content. */
public abstract class MaterialReport {

    private static final String LINE = "----------------------------------------";

    /**
     * TEMPLATE METHOD: the structure of the report is fixed (final)
     * and the only variable step is the body.
     */
    public final String generate(Material m, int elapsedDays) {
        return header() + body(m, elapsedDays) + footer();
    }

    /** Concrete step: valid by default, overridable if needed. */
    protected String header() {
        return LINE + System.lineSeparator()
             + "   BIBLIOTECH - NEXUS SOFTWARE" + System.lineSeparator()
             + LINE + System.lineSeparator();
    }

    /** Concrete step. */
    protected String footer() {
        return LINE + System.lineSeparator();
    }

    /** VARIABLE STEP: each report decides what it shows. */
    protected abstract String body(Material m, int elapsedDays);
}
package com.nexussoftware.bibliotech.presentation;

import com.nexussoftware.bibliotech.domain.Material;

/** A single line: for long listings. */
public class BriefReport extends MaterialReport {

    @Override
    protected String body(Material m, int elapsedDays) {
        return String.format("  %-28s %6.2f EUR%n",
                             m.getTitle(), m.calculateFine(elapsedDays));
    }
}
package com.nexussoftware.bibliotech.presentation;

import com.nexussoftware.bibliotech.domain.Material;

/** Full report: for incidents and complaints. */
public class DetailedReport extends MaterialReport {

    @Override
    protected String body(Material m, int elapsedDays) {
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("  %-14s %s%n",       "Title:",    m.getTitle()));
        sb.append(String.format("  %-14s %s%n",       "Type:",     m.getType()));
        sb.append(String.format("  %-14s %d days%n",  "Term:",     m.getLoanDays()));
        sb.append(String.format("  %-14s %d days%n",  "Delay:",    m.calculateDaysLate(elapsedDays)));
        sb.append(String.format("  %-14s %.2f EUR%n", "Fine:",     m.calculateFine(elapsedDays)));
        sb.append(String.format("  %-14s %s%n",       "Severity:", m.classifySeverity(elapsedDays)));
        return sb.toString();
    }
}
Material dvd = new Dvd("Refactoring Live", "DVD-0007", 95);

MaterialReport brief    = new BriefReport();
MaterialReport detailed = new DetailedReport();

System.out.print(brief.generate(dvd, 20));
System.out.print(detailed.generate(dvd, 20));
----------------------------------------
   BIBLIOTECH - NEXUS SOFTWARE
----------------------------------------
  Refactoring Live               8.50 EUR
----------------------------------------
----------------------------------------
   BIBLIOTECH - NEXUS SOFTWARE
----------------------------------------
  Title:         Refactoring Live
  Type:          DVD
  Term:          3 days
  Delay:         17 days
  Fine:          8.50 EUR
  Severity:      SEVERE
----------------------------------------

Four design details: generate is final (the structure is not negotiable); body is protected because it is an extension point for the subclasses and not part of the public API; header and footer are concrete and protected, so a future subclass can override them without being forced to; and the variable is declared MaterialReport, not BriefReport, so switching reports costs one word.

Solution 3

Case Decision Justification
1. Exportable to text Interface (Exportable with String toLine()) It is a capability that must be signed by unrelated classes (Material, Employee, Loan, MeetingRoom), and it requires no shared state
2. Base of three employee types Abstract class (Employee with abstract getMaxLoans()) They share fields (name, identifier, totalLoans), a constructor with validation and real logic; they form a genuine family and only one value varies
3. Comparing materials Interface, and moreover one from the JDK: Comparable<Material> It is pure behaviour without state, it already exists in the standard library and it must not consume the inheritance slot. It is applied in 04-04 and 05-09
4. Catalogue with several implementations Both: interface LoanCatalog plus base AbstractLoanCatalog The interface is the public type the business rules depend on (dependency inversion, 04-01); the abstract base saves the three implementations the repeated validation and formatting code, without forcing anyone to use it

Case 4 is the professional archetype and the one you will see again and again from module 5 onwards: the interface defines the type, the abstract class offers optional help, and the concrete classes choose.

Conclusion

You have completed the pair that holds up design in Java. You know that abstract on a class declares it conceptually incomplete and that it prevents new, without preventing you from using it as the type of variables, parameters and arrays. You know that an abstract method is an obligation passed downwards until a concrete subclass fulfils it, and — most importantly — why that obligation is worth more than any filler implementation: it turns a silent oversight, which surfaces weeks later as a miscalculated fine, into an instant compilation error.

You understand what an abstract class offers and an interface never will: instance fields, a constructor and protected members. You know what the constructor of a class that is never instantiated is for — it always runs via super(...), centralises the common validation and guarantees the invariants of the shared part — and why it is declared protected. You have the full comparison table between interface and abstract class, including the row that weighs most in practice: the abstract class consumes the only inheritance slot of whoever uses it, and that is why the professional path always starts with the interface.

And you have discovered the Template Method by writing it, not by reading it: a final method that fixes the skeleton of the algorithm and delegates the variable steps to abstract methods. That is what lets the fine formula — delay times rate, capped at €20 — exist once only in the whole of BiblioTech and produce different, correct results for a book, a magazine, a DVD and any format you add tomorrow. That pattern is formalised in 12-02. Finally, you can recognise the JDK's AbstractXxx pattern, where interface and abstract class do not compete but share the work: the former is the public type, the latter optional help.

BiblioTech is left with an honest hierarchy: Material is abstract and signs Lendable and Notifiable; Book, Magazine, Dvd mandatorily declare their type, their term and their rate; and MeetingRoom proves that you can sign the contract without joining the family.

But look at a piece you have been carrying since 03-07 and which is still unsatisfying: the incidents of a Loan are a String[] of loose texts. A real incident has a day, a reason and a severity, and deserves to be a type of its own. Now then, a top-level class, in its own file, for something that only makes sense inside a loan? In lesson 04-03, Inner Classes, you will see the four kinds of nested class Java offers, you will learn when each one is the right choice — including the one that causes real memory leaks in production — and you will turn those loose strings into a Loan.Incident with a name, a structure and encapsulation of its own.

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