انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-024-L02

الفئات المجرّدة (Abstract classes)

  • الدرس 2 من 6
  • 11 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

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 class and an abstract method.
  • Explain why new cannot 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

java
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 class marks the class as unfinished. You cannot create an object of it with new Account(...).
  • abstract on 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 new on the abstract class itself, but subclass constructors call them with super(owner, balance), exactly like any parent class.
  • Normal methods with bodies (deposit).
  • Abstract methods without bodies (monthlyFee).
  • static members, final methods, private helpers, and so on.

Subclasses must finish the job

java
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:

java
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:

java
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 Account is 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)

java
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):

text
Ana 150.0 fee 2.5
Omar fee 0.0

deposit was written once and both types use it.

Example 2: A fixed sequence with one abstract step (practical)

java
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();
    }
}
text
Ana: charged 0.0, balance 100.0
Omar: charged 2.5, balance 37.5

The 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)

java
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:

text
[sms] 555: Hi
[sms] 556: Hi again
2
error: recipient required

The 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

java
abstract class Account { abstract double monthlyFee(); }
class Gold extends Account { }
Account x = new Account();
text
error: Gold is not abstract and does not override abstract method monthlyFee() in Account
error: Account is abstract; cannot be instantiated

And the reverse mistake, an abstract method in a class that is not abstract:

text
error: Account is not abstract and does not override abstract method monthlyFee() in Account

Backend 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");

أظهر الحل 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 ____.

أظهر الحل A2

A2. body; semicolon.

A3. Can an abstract class have a constructor? What is it used for?

أظهر الحل 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).

أظهر الحل B1

B1. (Compiled and run.)

java
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?

أظهر الحل B2

B2. Shared: title, title(), summary(). Variable: pages().

B3. Predict the compiler message if Letter forgets to write pages().

أظهر الحل 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.

أظهر الحل 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?

أظهر الحل 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 new on it.
  • It can hold fields, constructors, normal methods, and abstract methods (a signature ending in a semicolon).
  • A concrete subclass must implement every abstract method, or be abstract itself.
  • Use abstract instead of an empty default when every subclass must supply its own behavior.
  • A normal final method 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

  1. Can you write new Account() when Account is abstract?
  2. What must a concrete subclass do about inherited abstract methods?
  3. Can an abstract class have fields and a constructor?
  4. When is abstract better than an empty method body in the parent?
  5. What is the main cost of making a class extend an abstract class?
أظهر الإجابات
  1. No. An abstract class cannot be instantiated.
  2. Implement every one of them, or be declared abstract too.
  3. Yes: fields of any access level, and constructors that subclasses call with super(...).
  4. When every subclass must define its own behavior, because forgetting it then becomes a compile error.
  5. 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.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.