Abstract classes
On This Lesson 11 topics
Lesson Overview
An interface is a pure contract: signatures and nothing else. Sometimes that is not enough. Savings accounts and checking accounts both have an owner and a balance, both accept deposits in exactly the same way, and differ only in one rule, the monthly fee. You do not want to copy the shared fields and code into every class, and you do not want an Account object that has no fee rule at all.
An abstract class is the answer: a parent class that is deliberately left unfinished. It holds the shared state and shared code, and it declares the missing pieces as abstract methods that every concrete subclass must complete. Think of a half-built house: the walls and plumbing are shared, and each buyer finishes the interior.
Why this matters: backend code is full of "same steps, one part differs". An abstract class lets you write the steps once and force each variant to supply only its part.
This lesson builds on extends, super(...), constructors, protected, final, and @Override from earlier modules, and on interfaces from lesson 1.
Learning Objectives
By the end of this lesson you will be able to:
- Declare an
abstract classand anabstractmethod. - Explain why
newcannot be used on an abstract class. - Give an abstract class fields, a constructor, and normal methods, and have subclasses call the constructor with
super(...). - Write concrete subclasses that implement every abstract method.
- Use a normal (or
final) method in the abstract class that calls an abstract one. - Read the compiler errors that appear when the rules are broken.
- Show how an abstract class can implement part of an interface.
Written Explanation
The syntax
abstract class Account {
protected final String owner;
protected double balance;
Account(String owner, double balance) {
this.owner = owner;
this.balance = balance;
}
public abstract double monthlyFee(); // no body: subclasses must write it
public void deposit(double amount) { balance += amount; } // shared code
}Two new things:
abstract classmarks the class as unfinished. You cannot create an object of it withnew Account(...).abstracton a method means "no body here, ends with a semicolon". Subclasses must supply one.
Per the Java tutorial, an abstract class may or may not contain abstract methods, but a class that has even one abstract method must itself be declared abstract.
What an abstract class can hold
Unlike an interface, an abstract class is a real class, so it can have:
- Fields that belong to each object (
owner,balance), with any access modifier. - Constructors. They are not used with
newon the abstract class itself, but subclass constructors call them withsuper(owner, balance), exactly like any parent class. - Normal methods with bodies (
deposit). - Abstract methods without bodies (
monthlyFee). staticmembers,finalmethods,privatehelpers, and so on.
Subclasses must finish the job
class CheckingAccount extends Account {
CheckingAccount(String owner, double balance) { super(owner, balance); }
@Override public double monthlyFee() { return 2.5; }
}A concrete (non-abstract) subclass has to implement every inherited abstract method. If it does not, it must be declared abstract too, and the job moves down to its own subclasses.
Abstract versus the empty default
In the previous module the parent had a "do nothing" monthEnd() so that subclasses without special behavior would still compile. The cost: a subclass that should have overridden it and forgot compiles and runs wrongly. Making the method abstract flips this. Forgetting becomes a compile error, so use it when every subclass must define its own behavior. Keep an ordinary default only when "do nothing" is a correct answer.
Shared steps, one variable part
An abstract class can fix a sequence of steps in a normal method and call the abstract method in the middle:
public final void monthEnd() {
double fee = monthlyFee(); // each subclass decides
balance -= fee;
System.out.println(owner + ": charged " + fee + ", balance " + balance);
}monthEnd() is written once. Marking it final stops subclasses from changing the sequence, while monthlyFee() is the one part they customize. This pattern is often called a template method. You do not need the name, but you will see it often.
The caller sees only the base type
Because Account is a type, polymorphism works as before:
Account[] all = { new Savings("Ana", 100), new Checking("Omar", 40) };
for (Account a : all) a.monthEnd();You can declare variables of the abstract type, but every object behind them is some concrete subclass.
Abstract classes and interfaces together
An abstract class may implement an interface without writing all of its methods. A concrete subclass must write the rest. This combination is common: the interface is the public contract, and the abstract class is an optional helper that does the boring shared part.
Limits
- A class has only one superclass. If
Accountis the parent, your class cannot also extend something else. This is the main cost of choosing an abstract class, and the next lesson builds on it. - An abstract class models "a kind of X" (see the is-a test from the inheritance module). If the classes are not truly one family, you are forcing a relationship that is not there.
Examples
Example 1: Shared fields, one rule differs (simple)
abstract class Account {
protected final String owner;
protected double balance;
Account(String owner, double balance) { this.owner = owner; this.balance = balance; }
public abstract double monthlyFee();
public void deposit(double amount) { balance += amount; }
}
class SavingsAccount extends Account {
SavingsAccount(String owner, double balance) { super(owner, balance); }
@Override public double monthlyFee() { return 0; }
}
class CheckingAccount extends Account {
CheckingAccount(String owner, double balance) { super(owner, balance); }
@Override public double monthlyFee() { return 2.5; }
}
public class Ex1 {
public static void main(String[] args) {
Account a = new CheckingAccount("Ana", 100);
a.deposit(50);
System.out.println(a.owner + " " + a.balance + " fee " + a.monthlyFee());
Account b = new SavingsAccount("Omar", 10);
System.out.println(b.owner + " fee " + b.monthlyFee());
}
}Output (JDK 21):
Ana 150.0 fee 2.5
Omar fee 0.0deposit was written once and both types use it.
Example 2: A fixed sequence with one abstract step (practical)
abstract class Account {
protected final String owner;
protected double balance;
Account(String owner, double balance) { this.owner = owner; this.balance = balance; }
protected abstract double monthlyFee();
public final void monthEnd() {
double fee = monthlyFee();
balance -= fee;
System.out.println(owner + ": charged " + fee + ", balance " + balance);
}
}
class Savings extends Account {
Savings(String o, double b) { super(o, b); }
@Override protected double monthlyFee() { return 0; }
}
class Checking extends Account {
Checking(String o, double b) { super(o, b); }
@Override protected double monthlyFee() { return 2.5; }
}
public class Ex2 {
public static void main(String[] args) {
Account[] all = { new Savings("Ana", 100), new Checking("Omar", 40) };
for (Account a : all) a.monthEnd();
}
}Ana: charged 0.0, balance 100.0
Omar: charged 2.5, balance 37.5The loop makes no type check and the base class guarantees the order of steps. A new account type needs only its own monthlyFee().
Example 3: An abstract class that implements part of an interface (realistic)
interface Notifier { void send(String to, String message); }
abstract class BaseNotifier implements Notifier {
private int sent = 0;
@Override public final void send(String to, String message) {
if (to == null || to.isBlank())
throw new IllegalArgumentException("recipient required");
deliver(to, message);
sent++;
}
protected abstract void deliver(String to, String message);
public int sentCount() { return sent; }
}
class ConsoleNotifier extends BaseNotifier {
@Override protected void deliver(String to, String message) {
System.out.println("[console] " + to + ": " + message);
}
}
class SmsNotifier extends BaseNotifier {
@Override protected void deliver(String to, String message) {
System.out.println("[sms] " + to + ": " + message);
}
}With main sending two messages through an SmsNotifier and then an empty recipient through a ConsoleNotifier:
[sms] 555: Hi
[sms] 556: Hi again
2
error: recipient requiredThe check and the counter are written once in BaseNotifier. Callers still see only Notifier. Notice this was optional help: ConsoleNotifier could have implemented Notifier directly.
The compiler's view
abstract class Account { abstract double monthlyFee(); }
class Gold extends Account { }
Account x = new Account();error: Gold is not abstract and does not override abstract method monthlyFee() in Account
error: Account is abstract; cannot be instantiatedAnd the reverse mistake, an abstract method in a class that is not abstract:
error: Account is not abstract and does not override abstract method monthlyFee() in AccountBackend Insight
Backend frameworks use abstract base classes mostly as helpers: they handle repeated chores (logging, validation, counting, error handling) and leave one method for you to write. Your code then plugs into a flow that the base class controls.
Since the base class controls the flow, changing it later affects every subclass. A change to monthEnd() or send() in a parent can alter behavior in many places at once, which is power and risk together. Keep abstract classes small, mark the fixed steps final, and prefer exposing a small interface to callers, as BaseNotifier does.
Common Mistakes
Mistake 1: Calling new on an abstract class
- Incorrect:
Account a = new Account("Ana", 0); - Correct:
Account a = new CheckingAccount("Ana", 0);
Declare variables as Account; create objects of a subclass.
Mistake 2: Writing a body for an abstract method
abstract double monthlyFee() { return 0; } is an error. A method is either abstract (semicolon) or has a body. If "return 0" is a sensible default, drop abstract.
Mistake 3: Forgetting an abstract method in a concrete subclass
The compiler reports that the class "is not abstract and does not override abstract method". Add the method with @Override, or declare the subclass abstract if it is meant to be finished further down.
Mistake 4: Using an abstract class when no state or code is shared
If the abstract class has no fields and no normal methods, it is only an interface with a weaker design: the subclass loses its one chance to extend something else. Use an interface.
Mistake 5: Letting subclasses break the sequence
If monthEnd() is not final, a subclass may override it and skip the fee. If the order of steps matters, make the method final.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Which of these lines compiles, given abstract class Document? (a) Document d = null; (b) Document d = new Document("A");
Show solution A1
A1. (a) compiles: a reference can be null or can refer to an object of a subclass. (b) does not compile: Document is abstract; cannot be instantiated.
A2. Fill the blanks: an abstract method has no ____ and ends with a ____.
Show solution A2
A2. body; semicolon.
A3. Can an abstract class have a constructor? What is it used for?
Show solution A3
A3. Yes. It cannot be run with new on the abstract class, but subclass constructors call it with super(...) to set up the shared fields.
Level B: Application
B1. Write abstract class Document with a title field set by a constructor, a method abstract int pages(), and a normal method summary() that returns the title and the page count. Add subclasses Report (number of sections times 4 pages) and Letter (always 1 page).
Show solution B1
B1. (Compiled and run.)
abstract class Document {
private final String title;
Document(String title) { this.title = title; }
String title() { return title; }
abstract int pages();
String summary() { return title + " (" + pages() + " pages)"; }
}
class Report extends Document {
private final int sections;
Report(String t, int sections) { super(t); this.sections = sections; }
@Override int pages() { return sections * 4; }
}
class Letter extends Document {
Letter(String t) { super(t); }
@Override int pages() { return 1; }
}With new Report("Q3", 5) and new Letter("Hello"), calling summary() on each printed Q3 (20 pages) and Hello (1 pages). The wording "1 pages" is a small bug you could fix later; it shows that tests should cover edge cases.
B2. In B1, which method is shared code and which is the variable part?
Show solution B2
B2. Shared: title, title(), summary(). Variable: pages().
B3. Predict the compiler message if Letter forgets to write pages().
Show solution B3
B3. Letter is not abstract and does not override abstract method pages() in Document.
Level C: Challenge
C1. Add public final String summary() to the design and explain, in one or two sentences, what you gain compared with the non-final version.
Show solution C1
C1. With final, subclasses cannot change the format of the summary, so every document reports itself in the same way and the only thing that can differ is pages(). Without final, a subclass could override summary() and make the output inconsistent.
C2. A teammate writes abstract class Printable { abstract void print(); } with no fields and no other methods. What would you suggest, and why?
Show solution C2
C2. Turn it into an interface (interface Printable { void print(); }). With no shared state or code, the abstract class only uses up each implementer's single superclass slot.
Mini Challenge
Write an abstract class Shipment with a destination set in the constructor, abstract double cost(), and a normal method label() that prints the destination and cost(). Add two subclasses: Standard (a flat price) and Express (a price per kilogram times a weight). In main, put one of each in an array of Shipment and call label() in a loop.
Check yourself: (1) label() appears only in Shipment; (2) main has no instanceof; (3) removing cost() from Express makes the compiler complain; (4) new Shipment(...) does not compile.
Summary & Next Lesson
Key takeaways
- An abstract class is a parent left unfinished on purpose. You cannot use
newon it. - It can hold fields, constructors, normal methods, and
abstractmethods (a signature ending in a semicolon). - A concrete subclass must implement every abstract method, or be abstract itself.
- Use
abstractinstead of an empty default when every subclass must supply its own behavior. - A normal
finalmethod that calls an abstract one fixes the sequence of steps and leaves one step open. - A class has only one superclass, so choosing an abstract class uses up that slot.
Next lesson
The next lesson, "Interface vs abstract class: decision guide", compares the two tools side by side and gives you a short checklist for picking the right one.
Self-check
- Can you write
new Account()whenAccountis abstract? - What must a concrete subclass do about inherited abstract methods?
- Can an abstract class have fields and a constructor?
- When is
abstractbetter than an empty method body in the parent? - What is the main cost of making a class extend an abstract class?
Show answers
- No. An abstract class cannot be instantiated.
- Implement every one of them, or be declared abstract too.
- Yes: fields of any access level, and constructors that subclasses call with
super(...). - When every subclass must define its own behavior, because forgetting it then becomes a compile error.
- The class has used its only superclass slot, so it cannot extend anything else.
References
- Oracle, The Java Tutorials: Abstract Methods and Classes. Supports the rules about abstract classes and methods, subclasses implementing all abstract methods, fields in abstract classes, and an abstract class implementing part of an interface.
- Oracle, The Java Tutorials: Defining an Interface. Supports the interface-as-contract background.
- All code, outputs, and compiler messages were produced by compiling and running the examples with JDK 21. The "template method" name is a common design-pattern term; this course's own explanation, not a cited definition.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.