Lesson 17 • Advanced
Interfaces & Abstraction Best Practices
Abstraction is how you hide messy detail behind a clean contract. Learn to design with abstract classes and interfaces so your code stays flexible, swappable, and easy to extend.
What You'll Learn in This Lesson
- ✓What abstraction is and why it keeps code flexible
- ✓Abstract classes and abstract methods — and why you can't instantiate them
- ✓The difference between abstract and concrete methods
- ✓How to design to an interface ("program to an interface")
- ✓The template method pattern and polymorphism through abstraction
- ✓How to choose between an abstract class and an interface
📚 Before You Start
This lesson focuses on design. You should already know the basic syntax of:
- Interfaces — declaring a contract and implementing it
- Inheritance —
extends, overriding, and polymorphism
The earlier Interfaces lesson taught the syntax. This one teaches you how to design with abstraction so your programs stay easy to change.
🔌 A Real-World Analogy: The Wall Socket
A wall socket is an abstraction. It promises one thing: "plug in here and you get power." It says nothing about how that power was made — coal, solar, wind, or a nuclear plant. Your kettle does not know and does not care.
That is the whole point. Because every appliance depends only on the socket contract, the power company can swap a coal plant for a wind farm and not a single kettle breaks. In code, the socket is your interface or abstract class; the power plants are the concrete classes that implement it.
The principle: depend on the stable contract (the socket), not the volatile detail (the power plant). That is the abstraction principle, and it is what keeps large programs from becoming impossible to change.
1️⃣ Abstract Classes & Abstract Methods
An abstract class is a class marked with the abstract keyword that is deliberately incomplete. It describes a general idea — a Payment, a Shape, an Animal — that is too vague to exist on its own. You can never write new Payment(...); the compiler stops you.
It becomes useful through abstract methods: method signatures with no body, ending in a semicolon. An abstract method is a promise: "every concrete subclass must supply this." The abstract class can also hold concrete methods (real, shared code) and fields, so common behaviour is written once.
💡 Why this matters: the abstract class captures everything the subtypes have in common, and the abstract methods mark exactly the parts that differ. You stop copy-pasting shared code, and the compiler forces every subclass to fill in the gaps.
In the example below, receipt() is shared by every payment, while authorize() is the one detail each payment type must define itself.
// "abstract" means: this class describes an idea, not a real thing.
// You can never write new Payment(...) — a Payment is too vague to exist.
// Subclasses fill in the missing detail (the abstract method) to become concrete.
abstract class Payment {
protected double amount;
Payment(double amount) { this.amount = amount; }
// ABSTRACT method: no body. Every concrete subclass MUST supply one.
abstract String authorize();
// CONCRETE method: shared by all subclasses, written once here.
void receipt() {
System.out.println(" Receipt: $" + String.format("%.2f", amount)
+ " via " + authorize());
}
}
// Concrete subclass: fills in authorize(), so it CAN be instantiated.
class CardPayment extends Payment {
CardPayment(double amount) { super(amount); }
String authorize() { return "card (3-D Secure)"; }
}
class CashPayment extends Payment {
CashPayment(double amount) { super(amount); }
String authorize() { return "cash (no network)"; }
}
public class Main {
public static void main(String[] args) {
// Payment p = new Payment(10); // ❌ won't compile: Payment is abstract
Payment[] payments = { new CardPayment(49.99), new CashPayment(5.00) };
for (Payment p : payments) {
p.receipt(); // calls the SHARED method; it dispatches to each authorize()
}
}
} Receipt: $49.99 via card (3-D Secure)
Receipt: $5.00 via cash (no network)2️⃣ Abstract vs Concrete — Knowing the Difference
The whole vocabulary of abstraction comes down to one split: which parts are fixed and shared, and which parts vary. Get this distinction clear and the rest follows.
| Term | Has a body? | Meaning |
|---|---|---|
| abstract method | No — ends in ; | A required gap; subclasses must fill it |
| concrete method | Yes | Shared behaviour, inherited as-is |
| abstract class | Mix of both | Incomplete idea — cannot be instantiated |
| concrete class | Every method | Complete — new works |
abstract or the compiler complains.3️⃣ Design to an Interface ("Program to an Interface")
The single most useful design rule in object-oriented code is: "program to an interface, not an implementation." In practice it means your variables, parameters, and return types use the abstract type, and the concrete class name appears only at the one spot where you call new.
✅ Depends on the contract
List<String> names = new ArrayList<>(); // swap to LinkedList anytime — callers // never notice
❌ Locked to a detail
ArrayList<String> names = new ArrayList<>(); // now you're stuck with ArrayList // everywhere
Because the method below takes a Notifier rather than an EmailNotifier, you can hand it any notifier — present or future — without changing a line. That flexibility is the payoff of designing to the abstraction.
import java.util.List;
// The ABSTRACTION: what a notifier does, not how. This is the "contract".
interface Notifier {
void send(String message);
}
// Two interchangeable implementations. The rest of the app never names these.
class EmailNotifier implements Notifier {
public void send(String message) {
System.out.println(" email -> " + message);
}
}
class SmsNotifier implements Notifier {
public void send(String message) {
System.out.println(" sms -> " + message);
}
}
public class Main {
// This method depends on the ABSTRACTION (Notifier), not a concrete class.
// Pass it ANY notifier and it just works — that's polymorphism through
// abstraction. New notifier types need ZERO changes here.
static void alertAll(List<Notifier> notifiers, String message) {
for (Notifier n : notifiers) {
n.send(message); // each one behaves differently behind one contract
}
}
public static void main(String[] args) {
// Program to the INTERFACE on the left, choose the implementation on the right.
List<Notifier> notifiers = List.of(new EmailNotifier(), new SmsNotifier());
alertAll(notifiers, "Server is back online");
}
} email -> Server is back online
sms -> Server is back online🔁 Polymorphism through abstraction: one call — n.send(message) — does something different for each implementation. The calling code stays the same; the behaviour changes. That is polymorphism, and abstraction is what makes it possible.
🎯 Your Turn #1: Implement the Shape Contract
Fill in the two blanks so Circle honours the Shape interface. You declare that it follows the contract, then supply the body the contract demands.
// 🎯 YOUR TURN — fill in the blanks marked with ___
// An abstraction for anything that has an area.
interface Shape {
double area(); // the contract: "give me your area"
}
// Make Circle promise to follow the Shape contract.
class Circle ___ Shape { // 👉 keyword that says "implements this interface"
private double radius;
Circle(double radius) { this.radius = radius; }
// 👉 provide the body the interface requires (use Math.PI * radius * radius)
public double area() { return ___; }
}
public class Main {
public static void main(String[] args) {
Shape s = new Circle(2.0); // hold it by the ABSTRACTION, not Circle
System.out.println("Area: " + String.format("%.2f", s.area()));
}
}
// ✅ Expected output:
// Area: 12.57Area: 12.57Hint: a class promises to follow an interface with the implements keyword, and a circle's area is Math.PI * radius * radius.
4️⃣ The Template Method Pattern
Sometimes the order of steps is what you want to reuse, while the steps themselves vary. The template method pattern captures exactly this: an abstract class writes one concrete method that lays out the algorithm's skeleton, calling abstract steps that subclasses fill in.
Mark the skeleton method final so subclasses can override the steps but never reorder the algorithm. A step can also have a sensible default (here, footer()) that subclasses may override only if they need to.
// TEMPLATE METHOD pattern: the abstract class fixes the STEPS (the algorithm),
// subclasses fill in the steps that vary. The "shape" of the work is reused;
// only the details change.
abstract class ReportGenerator {
// The template method is FINAL so subclasses can't change the order of steps.
final void generate() {
System.out.println(header());
for (String row : rows()) {
System.out.println(" " + row);
}
System.out.println(footer());
}
// Steps that vary — left abstract for subclasses to define.
abstract String header();
abstract String[] rows();
// A step with a sensible default — subclasses MAY override it.
String footer() { return " -- end of report --"; }
}
class SalesReport extends ReportGenerator {
String header() { return "SALES REPORT"; }
String[] rows() { return new String[]{ "Mon: $120", "Tue: $90" }; }
}
public class Main {
public static void main(String[] args) {
new SalesReport().generate(); // runs the FIXED algorithm, varied details
}
}SALES REPORT
Mon: $120
Tue: $90
-- end of report --Notice the win: generate() is written once and guarantees every report has a header, rows, then a footer — in that order. A new report type only fills in header() and rows().
🎯 Your Turn #2: Build an Abstract Animal
Make Animal an abstract class with an abstract sound() method, so the shared describe() method can call it. Dog supplies the concrete detail.
// 🎯 YOUR TURN — fill in the blanks marked with ___
// "abstract" makes Animal an idea you can't instantiate directly.
___ class Animal { // 👉 keyword that marks the class as abstract
private String name;
Animal(String name) { this.name = name; }
// 👉 declare an abstract method 'sound' returning String, with NO body
abstract String ___();
// Concrete, shared behaviour using the abstract step.
void describe() {
System.out.println(name + " says " + sound());
}
}
class Dog extends Animal {
Dog(String name) { super(name); }
String sound() { return "Woof"; } // the concrete detail Dog supplies
}
public class Main {
public static void main(String[] args) {
new Dog("Rex").describe();
}
}
// ✅ Expected output:
// Rex says WoofRex says WoofHint: the keyword that marks both the class and the method is the same word — abstract — and the abstract method ends with a semicolon, no braces.
5️⃣ Choosing: Abstract Class or Interface?
Both are tools for abstraction, and beginners agonise over which to use. Here is the honest comparison, followed by a rule that resolves most cases.
| Question | Interface | Abstract class |
|---|---|---|
| Can a type have many? | ✅ implement many | ❌ extend only one |
| Can it hold fields/state? | constants only | ✅ yes |
| Can it have a constructor? | ❌ no | ✅ yes |
| Shared method bodies? | default methods (Java 8+) | ✅ yes, freely |
| Best for… | a capability ("can do") | a base type with shared code ("is a") |
🧭 Decision rule: if subtypes share state or constructor logic, or you want a template method, reach for an abstract class. If you just need a contract of capabilities — especially one a class might mix with others — use an interface. When genuinely in doubt, prefer the interface: it keeps your options open because a class can implement many.
🧩 Mini-Challenge: A Discount Strategy
Now write it yourself from an outline only — no filled-in logic this time. Design a tiny Discount abstraction with two interchangeable implementations, and a checkout method that depends only on the abstraction.
public class Main {
// 🎯 MINI-CHALLENGE: Discount strategy through abstraction
// 1. Define an interface Discount with one method: double apply(double price)
// 2. Create TWO implementations:
// - PercentOff (constructor takes an int percent; returns price minus that %)
// - FlatOff (constructor takes a double amount; returns price minus that amount)
// 3. Write a method checkout(Discount d, double price) that prints the final price
// to 2 decimals — it must depend on the Discount ABSTRACTION, not a concrete class.
// 4. Call checkout with a PercentOff(20) and a FlatOff(15.0) on a price of 100.0
//
// ✅ Expected output:
// Final: $80.00
// Final: $85.00
public static void main(String[] args) {
// your code here
}
}If checkout mentions PercentOff or FlatOff by name, you have coupled to an implementation — change its parameter type back to Discount.
Common Errors & How to Fix Them
- ❌ Instantiating an abstract class:
new Payment(10)gives "Payment is abstract; cannot be instantiated." Create a concrete subclass (new CardPayment(10)) — you can still type the variable asPayment. - ❌ Leaky abstraction: if callers must downcast (
((StripeProcessor) p).stripeOnly()) or check the concrete type, the abstraction is leaking. Add the needed behaviour to the interface, or rethink the contract so callers never see the implementation. - ❌ Abstract class doing too much: a base class crammed with unrelated fields and methods forces every subclass to carry baggage it never uses. Keep abstract classes small and focused; push optional behaviour into separate interfaces.
- ❌ Over-abstraction: an interface with a single implementation that will never have another adds indirection for no benefit. Don't add an abstraction until you actually have two things to vary, or a real seam you need to swap.
- ❌ Forgot to implement an abstract method: "Dog is not abstract and does not override abstract method sound()." Either provide the method body in the subclass, or mark the subclass
abstracttoo.
Pro Tips
💡 Name abstractions by role, not by implementation: Notifier, Repository, Discount — never EmailNotifierInterface.
💡 Keep contracts small. A focused interface with two methods is easier to implement and reuse than one with twenty.
💡 Abstract is a design choice, not decoration. Introduce it when you have a real reason to vary something — a second implementation, a test double, a swappable provider.
📋 Quick Reference
| Concept | Syntax | Notes |
|---|---|---|
| Abstract class | abstract class Payment { … } | Cannot be instantiated |
| Abstract method | abstract String authorize(); | No body; subclass must override |
| Concrete method | void receipt() { … } | Shared, inherited as-is |
| Implement interface | class Circle implements Shape | Must define every method |
| Program to interface | List<String> x = new ArrayList<>(); | Abstract type on the left |
| Template method | final void generate() { … } | Fixed steps, varied details |
| Lock the algorithm | final void run() { … } | Subclass can't reorder steps |
❓ Frequently Asked Questions
What exactly is abstraction in Java?
Abstraction means exposing only what something does and hiding how it does it. You capture the 'what' in an abstract class or an interface — a contract of method signatures — and let concrete classes supply the 'how'. The rest of your code talks to the contract, so it does not need to know or care which implementation it is using.
What is the difference between an abstract method and a concrete method?
An abstract method has no body — just a signature ending in a semicolon — and every concrete subclass must provide an implementation. A concrete method has a real body and is inherited as-is, so it is written once and shared. Abstract methods are the parts that vary; concrete methods are the shared behaviour built on top of them.
Can you create an object of an abstract class?
No. Writing new Payment(...) on an abstract class is a compile error because the class is incomplete — it has at least one method with no body. You instantiate a concrete subclass instead, and you can still hold it in a variable typed as the abstract class (Payment p = new CardPayment(...)).
What does 'program to an interface, not an implementation' mean?
Declare variables, parameters and return types using the abstract type (the interface or abstract class), and only mention the concrete class at the single point where you create the object. For example List<String> names = new ArrayList<>(). Your code then depends on the contract, so you can swap ArrayList for LinkedList — or one payment provider for another — without touching the code that uses it.
When should I use an abstract class instead of an interface?
Use an abstract class when subclasses share state (fields) or constructor logic, or when you want a template method that fixes an algorithm's steps. Use an interface when you only need a contract of capabilities, especially since a class can implement many interfaces but extend only one class. A good rule of thumb: 'is-a' relationships with shared code lean towards an abstract class; pure 'can-do' capabilities lean towards an interface.
What is the template method pattern?
It is a classic use of abstraction where an abstract class defines the skeleton of an algorithm in one concrete method (often marked final), calling abstract steps that subclasses fill in. The overall order of steps is reused and locked down, while the varying details are overridden. The ReportGenerator example in this lesson is a template method.
🎉 Lesson Complete!
You can now design with abstraction, not just use it. You know how abstract classes and methods carve the shared from the varying, why an abstract class can't be instantiated, how to program to an interface so implementations stay swappable, and how the template method pattern and polymorphism fall out of good abstraction — plus how to choose between an abstract class and an interface.
Next: Inner Classes & Anonymous Classes — static, non-static, local, and anonymous inner classes, including the quick one-off implementations of the interfaces you just designed.
Sign up for free to track which lessons you've completed and get learning reminders.