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
abstracton classes: what it means and what it prevents- Abstract methods: the obligation that is inherited
- An abstract class WITH state: fields, constructor and concrete methods
- What is the constructor of a class that is never instantiated for?
- An incomplete subclass must also be abstract
- Interface versus abstract class: the full table
- A practical decision rule
- The Template Method pattern
- Combining the two:
AbstractXxx - BiblioTech:
Materialbecomes abstract - Refactoring
Book,MagazineandDvd - Common Mistakes and Tips
- Exercises
abstract on classes: what it means and what it prevents
abstract on classes: what it means and what it preventsThe abstract modifier applied to a class declares that the class is conceptually incomplete: it represents a general idea of which no concrete specimens exist.
With that single word, this stops compiling:
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.
- 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.
abstractis incompatible withprivate,staticandfinal. Withprivatebecause the subclass would not see it; withstaticbecause static methods are not polymorphic (03-06); withfinalbecausefinalforbids exactly whatabstractdemands.
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.
- 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.
- 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:
- Initialising the common state the subclass must not touch (the fields are
private). - Centralising validation. The title and reference checks are written once and applied by
Book,MagazineandDvd, with no way to skip them. - 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.
- 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 MaterialThe 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.
- 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.
- 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.
- 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.
- Combining the two:
AbstractXxx
AbstractXxxYou 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.
- BiblioTech:
Material becomes abstract
Material becomes abstractThis 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:
abstract class:new Material(...)no longer compiles.protectedconstructor: it tells the truth about who can call it.getType(),getLoanDays()andgetDailyRate()are abstract: the filler constantsLOAN_DAYS = 15andDAILY_RATE = 0.25disappear fromMaterial. Each format declares its own and nobody can forget them.calculateDaysLate,calculateFineandclassifySeverityarefinal: 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.
- Refactoring
Book, Magazine and Dvd
Book, Magazine and DvdNow 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 COMPILETYPE 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)returningheader() + body(m, days) + footer(). header()andfooter()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:
- Every BiblioTech object that can be exported to a text file.
- The common base of three employee types (
Internal,External,Intern) that share name, identifier and the loan limit, but with a different maximum. - The ability to compare two materials in order to sort them.
- 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():
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
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
- Console Input and Output
- Your First Complete Program: BiblioTech
Module 2: Control Flow
- Conditional Statements
- Loops
- Switch Statements
- Break and Continue
- Debugging and Execution Traces
- Project: The BiblioTech Interactive Menu
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- The Object Class: equals, hashCode and toString
Module 4: Advanced Object-Oriented Programming
- Interfaces
- Abstract Classes
- Inner Classes
- Anonymous Classes
- Lambda Expressions
- Functional Interfaces and Method References
- Enums and Records
Module 5: Data Structures and Collections
- Arrays
- The Collections Framework
- ArrayList
- LinkedList
- HashMap
- HashSet
- Queue and Deque
- Stack
- Sorting and Searching Collections
Module 6: Exception Handling
- Introduction to Exceptions
- The Try-Catch Block
- Throw and Throws
- Custom Exceptions
- The Finally Block
- Try-with-resources and AutoCloseable
- Error Handling Strategies and Logging
Module 7: File Input/Output
- Reading Files
- Writing Files
- File Streams
- BufferedReader and BufferedWriter
- Serialization
- The NIO.2 API: Path and Files
- Interchange Formats: CSV and Properties
Module 8: Multithreading and Concurrency
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
- Concurrent Collections and Atomic Variables
- Asynchronous Tasks with CompletableFuture
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection
- The Modern HTTP Client
Module 10: Advanced Topics
- Generics
- Annotations
- Reflection
- Java 8 Features: Streams and Optional
- Dates and Times with java.time
- Java 9 and Beyond
- Memory, Garbage Collection and Performance
Module 11: Java Frameworks and Libraries
- Introduction to Java Frameworks
- Spring Framework
- Hibernate
- JUnit
- Maven
- Advanced Testing with Mockito
- Essential Ecosystem Libraries
