Interfaces & Abstraction Best Practices

Reviewed & published by Brayan K

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.

Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

📚 Before You Start

This lesson focuses on design. You should already know the basic syntax of:

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()
        }
    }
}

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.

TermHas a body?Meaning
abstract methodNo — ends in ;A required gap; subclasses must fill it
concrete methodYesShared behaviour, inherited as-is
abstract classMix of bothIncomplete idea — cannot be instantiated
concrete classEvery methodComplete — new works

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

🔁 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.57

Hint: 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
    }
}

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 Woof

Hint: 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.

QuestionInterfaceAbstract 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

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

ConceptSyntaxNotes
Abstract classabstract class Payment { … }Cannot be instantiated
Abstract methodabstract String authorize();No body; subclass must override
Concrete methodvoid receipt() { … }Shared, inherited as-is
Implement interfaceclass Circle implements ShapeMust define every method
Program to interfaceList<String> x = new ArrayList<>();Abstract type on the left
Template methodfinal void generate() { … }Fixed steps, varied details
Lock the algorithmfinal void run() { … }Subclass can't reorder steps

🎉 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.

Practice quiz

What happens when you write new Payment(10) where Payment is an abstract class?

  • It runs normally
  • It returns null
  • Compile error: Payment is abstract; cannot be instantiated
  • It throws at runtime

Answer: Compile error: Payment is abstract; cannot be instantiated. You can never instantiate an abstract class. Create a concrete subclass like new CardPayment(10) instead.

What is an abstract method?

  • A method signature with no body, ending in a semicolon
  • A method with a body that cannot be overridden
  • A static method on an interface
  • A private helper method

Answer: A method signature with no body, ending in a semicolon. An abstract method has no body — just a signature ending in ';' — and every concrete subclass must implement it.

Which keyword makes a class promise to follow an interface's contract?

  • extends
  • inherits
  • uses
  • implements

Answer: implements. A class uses 'implements' to follow an interface, and must define every method the interface declares.

What does 'program to an interface, not an implementation' mean?

  • Never use concrete classes
  • Use the abstract type for variables/params, naming the concrete class only at 'new'
  • Always use abstract classes over interfaces
  • Put all logic in the interface

Answer: Use the abstract type for variables/params, naming the concrete class only at 'new'. Declare variables, parameters, and return types with the abstract type so implementations stay swappable.

How many classes can a Java class extend, and how many interfaces can it implement?

  • One class, many interfaces
  • One class, one interface
  • Many classes, one interface
  • Many classes, many interfaces

Answer: One class, many interfaces. A class extends only one class but can implement many interfaces — a key reason to favour interfaces when in doubt.

In the template method pattern, why is the skeleton method often marked final?

  • So it runs faster
  • So it can be static
  • So subclasses cannot reorder or change the algorithm's steps
  • So it returns void

Answer: So subclasses cannot reorder or change the algorithm's steps. final locks the algorithm: subclasses fill in the abstract steps but can't change the fixed order of the template.

When is a subclass of an abstract class itself concrete (instantiable)?

  • Always
  • Only when every inherited abstract method has a body
  • Only when it adds new fields
  • Never

Answer: Only when every inherited abstract method has a body. A class is concrete only when every abstract method it inherits is implemented; otherwise it must be marked abstract too.

For a Circle implementing Shape, what is the correct body of area()?

  • return radius * radius;
  • return 2 * Math.PI * radius;
  • return Math.PI * radius;
  • return Math.PI * radius * radius;

Answer: return Math.PI * radius * radius;. A circle's area is Math.PI * radius * radius (pi r squared). For radius 2 that prints 12.57.

When should you prefer an abstract class over an interface?

  • When you only need a contract of capabilities
  • When subtypes share state or constructor logic
  • When a class must mix many types
  • Never — interfaces are always better

Answer: When subtypes share state or constructor logic. Abstract classes can hold fields, constructors, and shared method bodies — ideal when subtypes share state or a template.

What enables one call like n.send(message) to behave differently per implementation?

  • Encapsulation
  • Static binding
  • Polymorphism through abstraction
  • Generics

Answer: Polymorphism through abstraction. Calling a method on an abstract type dispatches to each concrete implementation — that is polymorphism via abstraction.

Continue this course

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.

Related lessons