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

الواجهة مقابل الفئة المجرّدة: دليل القرار

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

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

Lesson Overview

You now have two ways to say "these classes share something": an interface (a pure contract) and an abstract class (an unfinished parent with state and code). They look similar from the outside, and beginners often pick one by guessing. This lesson replaces the guess with a short list of questions.

Think of the wall socket again. The socket is the interface: any device with the right plug works. An abstract class is more like a device family that shares one chassis, such as every model in a line of printers built on the same body. Many unrelated devices fit one socket, while only closely related models share one chassis.

Why this matters: the choice is hard to undo. Once many classes extend an abstract class, moving to an interface means editing all of them. In a backend project, a wrong choice shows up months later as a design that is hard to test or extend.

This lesson builds on lessons 1 and 2 of this module.

Learning Objectives

By the end of this lesson you will be able to:

  • List the technical differences between an interface and an abstract class.
  • Apply four questions that decide between them.
  • Choose correctly for unrelated types that share a capability.
  • Choose correctly for a family of classes that share fields and code.
  • Combine the two: an interface plus an optional abstract helper.
  • Explain why "start with an interface" is a sensible default.

Written Explanation

The technical differences

QuestionInterfaceAbstract class
Can you use new on it?NoNo
Per-object fields (state)?No: fields are constantsYes, any access modifier
Constructors?NoYes (called through super(...))
Method bodies?Only default and static (next lesson)Normal methods plus abstract ones
How many can one class use?Any number (implements A, B, C)One (extends)
Method accessMethods are publicpublic, protected, package-private, or private

This follows the comparison in the Java tutorial, which also notes that one library class, HashMap, extends an abstract class and implements several interfaces. Both tools are used together all the time.

Four questions

Ask these in order.

  1. Do the classes share per-object state or real code? (fields such as balance, or logic that is the same in all of them) If yes, an abstract class is a candidate. If they share nothing but a promise, use an interface.
  2. Are the classes one family? The is-a test: a checking account is a kind of account. If a "kind of" relationship is natural, an abstract class fits. If the classes are unrelated (an invoice and a photo can both be printed), use an interface.
  3. Does a class need several roles? A class can implement Notifier and Auditable at once, but it can extend only one class. If a class might need more than one role, give the roles as interfaces. Keep the single extends slot free.
  4. Do you need protected members or non-public methods? Interface methods are public. If you want helper methods visible only to subclasses, they belong in an abstract class.

The default: start with an interface

When two answers point in different directions, begin with an interface for what callers need. The reasons:

  • It costs nothing: no state, no parent to inherit from.
  • It is easy to replace in tests with a tiny fake.
  • You can add an abstract helper later without changing any caller, because callers still see the interface. The reverse change (turning an abstract class into an interface) breaks every caller that uses the abstract type.

Add an abstract class only when you can name concrete state or code that would otherwise be copied into several classes.

Combining the two

The best-known design is both:

text
interface Exporter            <- what callers see
abstract class BaseExporter   <- optional shared help (validation, common steps)
class CsvExporter extends BaseExporter
class UpperExporter implements Exporter   <- does not need the helper

Callers hold Exporter. A class that wants the shared code extends the helper. A class that does not, implements the interface directly. This is exactly what the notifier helper in the last lesson did.

Common signals

SignalLikely tool
"can do X", "can be Y" (a capability or role)Interface
"is a kind of X" with shared fieldsAbstract class
Unrelated classes in different packagesInterface
You are about to copy-paste the same fields into three classesAbstract class
You want to swap an implementation in testsInterface
The class already extends somethingInterface (the only choice)

Where the rule bends

These guides are not laws. An abstract class with no state and no real code is an interface in disguise, so switch. An interface with so many methods that every implementer copies the same bodies is hiding a shared base class, so add a helper. Reasonable engineers sometimes disagree. Aim for the design that keeps callers independent of the details.

Examples

Example 1: Unrelated types, one capability (simple)

java
interface Printable { String render(); }
class Invoice implements Printable {
    public String render() { return "INVOICE #7"; }
}
class Photo implements Printable {
    private final int width, height;
    Photo(int w, int h) { width = w; height = h; }
    public String render() { return "PHOTO " + width + "x" + height; }
}
public class Ex1 {
    static void printAll(Printable[] items) {
        for (Printable p : items) System.out.println(p.render());
    }
    public static void main(String[] args) {
        printAll(new Printable[] { new Invoice(), new Photo(800, 600) });
    }
}
text
INVOICE #7
PHOTO 800x600

An invoice and a photo have nothing in common except being printable: interface.

Example 2: A family sharing state (practical)

java
abstract class Vehicle {
    protected final String id;
    protected double fuel;
    private double speed;
    Vehicle(String id, double fuel) { this.id = id; this.fuel = fuel; }
    void accelerate(double by) { if (fuel > 0) speed += by; }
    double speed() { return speed; }
    abstract double fuelPerKm();
    boolean canTravel(double km) { return fuel >= km * fuelPerKm(); }
}
class Car extends Vehicle {
    Car(String id, double fuel) { super(id, fuel); }
    @Override double fuelPerKm() { return 0.07; }
}
class Truck extends Vehicle {
    Truck(String id, double fuel) { super(id, fuel); }
    @Override double fuelPerKm() { return 0.3; }
}

With one Car and one Truck, each given 10 units of fuel and accelerated by 30, then asked about a 100 km trip:

text
C1 30.0 true
T1 30.0 false

(fuelPerKm values here are made-up teaching numbers.) The fields and the accelerate rule are shared, and fuelPerKm() is the one variable part: abstract class. An interface could not hold fuel and speed.

Example 3: Both together (realistic)

java
interface Exporter { String export(String data); String extension(); }

abstract class BaseExporter implements Exporter {
    @Override public String export(String data) {
        if (data == null || data.isEmpty())
            throw new IllegalArgumentException("nothing to export");
        return format(data);
    }
    protected abstract String format(String data);
}
class CsvExporter extends BaseExporter {
    protected String format(String d) { return String.join(",", d.split(" ")); }
    public String extension() { return "csv"; }
}
class UpperExporter implements Exporter {   // does not need the helper
    public String export(String d) { return d.toUpperCase(); }
    public String extension() { return "txt"; }
}

Calling export("a b c") on each, then printing the extension:

text
a,b,c.csv
A B C.txt

Callers use only Exporter. The helper is a convenience, not a requirement. A third class could implement Exporter directly tomorrow.

And a reminder of the single-parent rule. Writing class C extends A, B { } gives:

text
error: '{' expected

The compiler stops at the comma: only one class can follow extends.

Backend Insight

The interface-plus-optional-helper design appears across backend libraries: the interface is the stable public contract, and the abstract class is the implementer's shortcut. For your own code, the practical rule is to expose interfaces at the boundaries (what a controller or service depends on) and use abstract classes inside a package for shared implementation.

A related habit is keeping the one extends slot free. Frameworks sometimes ask you to extend one of their classes. If your own class has already used its slot for a base class of your own, you are stuck. Interfaces never run out.

Common Mistakes

Mistake 1: Choosing an abstract class "just in case"

A base class with no fields and no shared code costs the subclass its only parent. Start with an interface and add a helper once real duplication appears.

Mistake 2: Using an interface when state is really shared

If every implementation declares the same balance field and copies the same deposit method, the interface is hiding a missing base class. Add an abstract class.

Mistake 3: Treating unrelated types as a family

Making Invoice and Photo extend a common Printable class forces a relationship that does not exist. The test: can you say "an Invoice is a kind of Printable"? If it reads as "can be printed", it is a capability: interface.

Mistake 4: Letting callers depend on the helper

If methods take BaseExporter instead of Exporter, classes that implement the interface directly are shut out. Declare parameters and fields with the interface type.

Mistake 5: Growing a god interface

Putting every method any implementer might need into one interface leads to empty or throwing methods. Split by role.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. Name two things an abstract class can have that an interface cannot.

أظهر الحل A1

A1. Any two of: per-object fields, constructors, protected or private members, normal (non-default) methods with bodies.

A2. Complete: a class can ____ many interfaces but ____ only one class.

أظهر الحل A2

A2. implement; extend.

A3. Choose interface or abstract class for each: (a) Searchable, a capability that a Book, a Contact, and a Folder can all have; (b) a base Animal with name and age fields shared by Dog and Cat.

أظهر الحل A3

A3. (a) Interface: three unrelated types share a capability. (b) Abstract class: a family with shared state.

Level B: Application

B1. For each scenario, apply the four questions and decide: (a) payment methods (card, cash, bank transfer) that each need a pay(double) method and share nothing else; (b) report generators that all need the same title field, the same header-printing code, and a different body; (c) a Logger role that any class in the system may offer.

أظهر الحل B1

B1. (a) Interface (Payment with pay): nothing shared. (b) Abstract class: shared field and header code, with an abstract method for the body. Optionally put an interface in front of it. (c) Interface: it is a role that any class may offer, and it must not use up a superclass slot.

B2. A teammate wrote abstract class Auditable { abstract String auditId(); } and class Order extends Auditable. Later, Order must extend Document. What goes wrong, and how do you fix it?

أظهر الحل B2

B2. Order cannot extend two classes, and Auditable has no state or code anyway. Change it to interface Auditable { String auditId(); } and write class Order extends Document implements Auditable.

B3. Draw (in text) a design for exporting a customer list to CSV and JSON, with an optional helper that refuses empty lists.

أظهر الحل B3

B3.

text
interface CustomerExporter { String export(List<Customer> list); }
abstract class BaseCustomerExporter implements CustomerExporter   // optional helper
    - export(list): refuse empty, then call format(list)
    - protected abstract String format(List<Customer> list)
class CsvCustomerExporter extends BaseCustomerExporter
class JsonCustomerExporter extends BaseCustomerExporter

(List is only previewed here as a name; the collections module teaches it. A plain array would also do.)

Level C: Challenge

C1. A class design has interface Account { double balance(); void deposit(double a); } and three implementations, each declaring private double balance and the same deposit body. Explain what is wrong and how you would restructure it while keeping callers unchanged.

أظهر الحل C1

C1. The three classes copy the same state and code, so a change must be made three times. Keep the interface, and add abstract class BaseAccount implements Account holding balance and deposit; the three classes extend it. Callers still use Account, so nothing changes for them.

C2. "Always use abstract classes, because they can do everything interfaces can." Give two reasons this is wrong.

أظهر الحل C2

C2. (1) A class has only one superclass, so a class that already extends something cannot use another abstract class, while it can implement any number of interfaces. (2) Abstract classes force a "kind of" relationship, so unrelated types (an invoice and a photo) cannot share a capability without being put under an artificial parent.

Mini Challenge

You are designing "things that can be saved": a Customer, an Order, and an AuditLog. Customer and Order are both domain objects with an id and a createdAt value; the audit log has neither.

  1. Decide what is an interface and what is an abstract class. Write the names in a three-line sketch.
  2. Write the one or two questions from the guide that justify each choice.
  3. State what would happen to your design if next month Customer has to extend a library class.

Check: your sketch has at most one abstract class, and callers depend only on the interface.

Summary & Next Lesson

Key takeaways

  • Interface: capability or role, no instance state, many per class, public methods.
  • Abstract class: shared base with state and real code, one per class, any access level.
  • Ask: shared state or code? one family? several roles? non-public members needed?
  • Default: start with an interface. Add an abstract class only for real, named duplication.
  • The two combine: callers see the interface, and an abstract helper is optional help for implementers.
  • Declare variables and parameters with the interface type so that classes that skip the helper are not shut out.

Next lesson

The next lesson, "Default and static interface methods", shows how an interface can carry a little code of its own, why Java added that, and what risks come with it.

Self-check

  1. Two unrelated classes need to share a capability. Which tool?
  2. Closely related classes share fields and code. Which tool?
  3. Why can a class not extend two abstract classes?
  4. What is the benefit of "interface plus optional abstract helper"?
  5. Why is the default "start with an interface"?
أظهر الإجابات
  1. An interface.
  2. An abstract class.
  3. Java allows only one superclass per class. Only interfaces can be combined.
  4. Callers depend only on the interface, and implementers who want the shared code can extend the helper while others skip it.
  5. It adds no state or parent, it is easy to fake in tests, and a helper can be added later without changing callers.

References

  • Oracle, The Java Tutorials: Abstract Methods and Classes. Supports the comparison table, the "when to use which" guidance, and the HashMap example of using both.
  • Oracle, The Java Tutorials: Defining an Interface. Supports the idea of interfaces as contracts.
  • All code and output above were produced by compiling and running the examples with JDK 21. The four-question checklist and the "start with an interface" default are this course's own teaching guidance, not a rule from the Java specification.

التطبيق

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

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

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

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