Interface vs abstract class: decision guide
On This Lesson 11 topics
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
| Question | Interface | Abstract class |
|---|---|---|
Can you use new on it? | No | No |
| Per-object fields (state)? | No: fields are constants | Yes, any access modifier |
| Constructors? | No | Yes (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 access | Methods are public | public, 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.
- 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. - 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.
- Does a class need several roles? A class can implement
NotifierandAuditableat once, but it can extend only one class. If a class might need more than one role, give the roles as interfaces. Keep the singleextendsslot free. - Do you need
protectedmembers 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:
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 helperCallers 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
| Signal | Likely tool |
|---|---|
| "can do X", "can be Y" (a capability or role) | Interface |
| "is a kind of X" with shared fields | Abstract class |
| Unrelated classes in different packages | Interface |
| You are about to copy-paste the same fields into three classes | Abstract class |
| You want to swap an implementation in tests | Interface |
| The class already extends something | Interface (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)
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) });
}
}INVOICE #7
PHOTO 800x600An invoice and a photo have nothing in common except being printable: interface.
Example 2: A family sharing state (practical)
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:
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)
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:
a,b,c.csv
A B C.txtCallers 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:
error: '{' expectedThe 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.
Show solution 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.
Show solution 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.
Show solution 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.
Show solution 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?
Show solution 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.
Show solution B3
B3.
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.
Show solution 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.
Show solution 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.
- Decide what is an interface and what is an abstract class. Write the names in a three-line sketch.
- Write the one or two questions from the guide that justify each choice.
- State what would happen to your design if next month
Customerhas 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
- Two unrelated classes need to share a capability. Which tool?
- Closely related classes share fields and code. Which tool?
- Why can a class not extend two abstract classes?
- What is the benefit of "interface plus optional abstract helper"?
- Why is the default "start with an interface"?
Show answers
- An interface.
- An abstract class.
- Java allows only one superclass per class. Only interfaces can be combined.
- Callers depend only on the interface, and implementers who want the shared code can extend the helper while others skip it.
- 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
HashMapexample 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.
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.