Abstraction

Su Suriya Ravichandran Updated 14 Nov 2026
8 min read ·Lesson 53 of 133

Abstraction means exposing what an object does while hiding how it does it. A caller works with a simplified, essential view: "send this message," "store this file," "charge this amount." The details of protocols, file systems, and payment networks stay behind that view. Java supports abstraction directly through abstract classes and interfaces, which let you define a type by its operations without committing to an implementation.

Abstraction in Everyday Code

You already use abstraction constantly:

List<String> names = new ArrayList<>();
names.add("Ada");
names.get(0);

You know what add and get do. You do not need to know that ArrayList stores elements in an internal array that it grows by copying, or that LinkedList uses chained nodes instead. The List interface defines the operations; each implementation decides how to provide them. Code written against List works with any of them.

Abstract Classes

An abstract class is a class marked abstract. It cannot be instantiated directly, and it may contain abstract methods: methods with a signature but no body, which subclasses must implement.

public abstract class Report {
    private final String title;

    protected Report(String title) {
        this.title = title;
    }

    // abstract: every report must supply its own body content
    protected abstract String body();

    // concrete: shared by every report
    public final String render() {
        return "=== " + title + " ===\n" + body() + "\n--- end of report ---";
    }
}
public class SalesReport extends Report {
    private final double total;

    public SalesReport(double total) {
        super("Sales");
        this.total = total;
    }

    @Override
    protected String body() {
        return String.format("Total sales: %.2f", total);
    }
}

public class AttendanceReport extends Report {
    private final int present, enrolled;

    public AttendanceReport(int present, int enrolled) {
        super("Attendance");
        this.present = present;
        this.enrolled = enrolled;
    }

    @Override
    protected String body() {
        return present + " of " + enrolled + " students present";
    }
}
Report r1 = new SalesReport(48250.5);
Report r2 = new AttendanceReport(27, 30);
System.out.println(r1.render());
System.out.println(r2.render());

Report r3 = new Report("Draft");    // compile error: Report is abstract; cannot be instantiated

Output:

=== Sales ===
Total sales: 48250.50
--- end of report ---
=== Attendance ===
27 of 30 students present
--- end of report ---

How it works

  • abstract class Report says: "this is an incomplete type, meant to be extended."
  • protected abstract String body(); declares a method every concrete subclass must provide. It ends with a semicolon instead of a body.
  • render() is an ordinary method that calls body(). When it runs on a SalesReport, the SalesReport version of body() executes.
  • Abstract classes can have fields, constructors, and concrete methods. The constructor runs when a subclass calls super(...).

Rules for abstract classes and methods

  • A class with even one abstract method must itself be declared abstract.
  • A subclass must implement every inherited abstract method, or be declared abstract itself.
  • An abstract method cannot be private (subclasses could not implement it), static (static methods are not overridden), or final (it must be overridden).
  • An abstract class may have no abstract methods; marking it abstract simply prevents direct instantiation.

The pattern in render(), where a fixed algorithm calls abstract steps that subclasses fill in, is called the Template Method pattern. Making render() final guarantees every report has the same frame, while each subclass controls only its own content.

Interfaces

An interface defines a set of methods a class promises to provide, with no instance state at all. It describes a capability or role:

public interface Storage {
    void save(String key, byte[] data);
    byte[] load(String key);
    boolean exists(String key);
}

Interface methods are public and abstract by default, so the modifiers are omitted. A class implements the interface by providing every method:

import java.util.HashMap;
import java.util.Map;

public class InMemoryStorage implements Storage {
    private final Map<String, byte[]> data = new HashMap<>();

    @Override
    public void save(String key, byte[] bytes) { data.put(key, bytes.clone()); }

    @Override
    public byte[] load(String key) {
        byte[] b = data.get(key);
        return b == null ? null : b.clone();
    }

    @Override
    public boolean exists(String key) { return data.containsKey(key); }
}

Another class could implement the same interface by writing to disk, to Amazon S3, or to a database. Code that depends only on Storage works with any of them:

public class AvatarService {
    private final Storage storage;

    public AvatarService(Storage storage) {    // any Storage implementation
        this.storage = storage;
    }

    public void upload(String userId, byte[] image) {
        storage.save("avatars/" + userId, image);
    }
}

AvatarService service = new AvatarService(new InMemoryStorage());   // tests
// new AvatarService(new S3Storage(...))                            // production

AvatarService neither knows nor cares where images go. Switching storage providers, or using an in-memory fake in tests, requires no change to AvatarService.

What interfaces can contain

MemberSinceNotes
Abstract methodsJava 1.0Implicitly public abstract
ConstantsJava 1.0Implicitly public static final
default methodsJava 8Have a body; implementing classes inherit or override them
static methodsJava 8Utility methods called on the interface name
private methodsJava 9Helpers shared by default methods
public interface Shape {
    double area();
    double perimeter();

    default String summary() {                     // inherited by every implementation
        return String.format("%s: area %.2f, perimeter %.2f",
                getClass().getSimpleName(), area(), perimeter());
    }

    static Shape square(double side) {             // factory on the interface
        return new Rectangle(side, side);
    }
}

public record Rectangle(double w, double h) implements Shape {
    public double area() { return w * h; }
    public double perimeter() { return 2 * (w + h); }
}

System.out.println(Shape.square(3).summary());   // Rectangle: area 9.00, perimeter 12.00

Default methods let interfaces evolve: a new method can be added with a default implementation without breaking existing implementing classes. That is how List gained sort, forEach, and removeIf in Java 8 without breaking every list ever written.

Multiple interfaces

A class extends only one class but can implement many interfaces:

public class SmartCamera implements Recordable, Streamable, Configurable { ... }

Each interface describes one capability, and the class combines them. This is how Java provides multiple inheritance of type without the ambiguity of multiple inheritance of state.

Abstract Class or Interface?

Abstract classInterface
Instance fields (state)YesNo (constants only)
ConstructorsYesNo
Method implementationsAnydefault, static, private only
A class can extend/implementOneMany
Access levels for methodsAnypublic (or private helpers)
Expresses"is a kind of", with shared code and state"can do", a capability or contract
Typical examplesAbstractList, HttpServlet, InputStreamList, Comparable, Runnable, AutoCloseable

Guidelines:

  • Default to an interface for defining a type. It imposes the fewest restrictions on implementers.
  • Use an abstract class when implementations genuinely share state or substantial code, or when you need protected members or constructors.
  • The two combine well: an interface defines the type, and an abstract class provides a convenient partial implementation (the JDK's List and AbstractList follow exactly this pattern).

Levels of Abstraction

Abstraction is not only about abstract classes and interfaces. Every well-named method is an abstraction:

public void enroll(Student s, Course c) {
    checkPrerequisites(s, c);
    reserveSeat(c);
    chargeFee(s, c.fee());
    sendConfirmation(s, c);
}

A reader understands enrollment from this method alone. Each step hides its own details, which in turn may call further methods hiding more details. Keeping each method at a single level of abstraction (all high-level steps, or all low-level detail, not a mix) makes code much easier to read.

Abstraction Versus Encapsulation

The two are related but answer different questions:

AbstractionEncapsulation
FocusWhat the outside world sees: the essential operationsProtecting internal data and enforcing rules
Main toolsInterfaces, abstract classes, well-chosen method namesprivate fields, controlled methods, validation
Question answered"What can this do?""Who is allowed to change this, and how?"

A well-designed class uses both: it presents a simple abstraction to callers and encapsulates the data that makes the abstraction work.

Practical Example: Exporting Data

import java.util.List;
import java.util.StringJoiner;

public class ExportDemo {
    record Student(String name, int marks) {}

    interface Exporter {
        String fileExtension();
        String export(List<Student> students);
    }

    static abstract class TextExporter implements Exporter {
        @Override
        public final String export(List<Student> students) {
            StringBuilder out = new StringBuilder(header());
            for (Student s : students) {
                out.append(row(s)).append('\n');
            }
            return out.toString();
        }

        protected abstract String header();
        protected abstract String row(Student s);
    }

    static class CsvExporter extends TextExporter {
        public String fileExtension() { return "csv"; }
        protected String header() { return "name,marks\n"; }
        protected String row(Student s) { return s.name() + "," + s.marks(); }
    }

    static class MarkdownExporter extends TextExporter {
        public String fileExtension() { return "md"; }
        protected String header() { return "| Name | Marks |\n|---|---|\n"; }
        protected String row(Student s) { return "| " + s.name() + " | " + s.marks() + " |"; }
    }

    static class JsonExporter implements Exporter {
        public String fileExtension() { return "json"; }
        public String export(List<Student> students) {
            StringJoiner j = new StringJoiner(",", "[", "]");
            for (Student s : students) {
                j.add("{\"name\":\"" + s.name() + "\",\"marks\":" + s.marks() + "}");
            }
            return j.toString();
        }
    }

    static void save(Exporter exporter, List<Student> data) {
        System.out.println("report." + exporter.fileExtension());
        System.out.println(exporter.export(data));
    }

    public static void main(String[] args) {
        List<Student> data = List.of(new Student("Ada", 91), new Student("Linus", 84));
        save(new CsvExporter(), data);
        save(new MarkdownExporter(), data);
        save(new JsonExporter(), data);
    }
}

Output:

report.csv
name,marks
Ada,91
Linus,84

report.md
| Name | Marks |
|---|---|
| Ada | 91 |
| Linus | 84 |

report.json
[{"name":"Ada","marks":91},{"name":"Linus","marks":84}]

The Exporter interface is the abstraction save depends on: a file extension and an export operation. TextExporter is an abstract class that implements the shared line-by-line algorithm once and leaves header and row abstract. CSV and Markdown fill in those two steps. JSON does not fit the line-by-line shape, so it implements Exporter directly. save works with all three, and a new format (XML, PDF) needs only a new class. The JSON writer here does not escape quotes in names; production code would use a JSON library.

0 Comments

Reviewed before they appear

No comments yet.

Java
Java Introduction Java Features Java Syntax Java Comments Java Statements Java Output Java Variables Java Data Types Java Type Casting Java Operators Java Strings Java Numbers Java Math Java Booleans Java User Input Java Conditions Java If Else Java Switch Java While Loop Java For Loop Java Break Java Continue Java Arrays Java Methods Java Parameters Java Return Values Java Scope Java Recursion Install Java Install JDK Java IDEs Java JDK Java JRE Java JVM Java Compiler Java File Compile Java Run Java JShell Basics Java Packages Java OOP Java Classes Java Objects Class Methods Constructors Constructor Overload this Keyword static Keyword final Keyword Encapsulation Inheritance Polymorphism Abstraction Access Modifiers super Keyword Method Overload Method Override Instanceof Operator Nested Classes Inner Classes Anonymous Classes Enums Records Array Basics Array Length Loop Arrays Multidimensional Arrays Array Copy Array Sorting Array Search ArrayList LinkedList HashSet TreeSet HashMap TreeMap Queue Interface Deque Interface Stack Class Iterator List Interface Set Interface Map Interface Collections Class Comparable Comparator Generics Basics Generic Classes Generic Methods Wildcards String Basics String Length String Methods String Concatenation String Compare String Search String Replace String Split String Format StringBuilder StringBuffer Text Blocks Regular Expressions Number Types Integer Class Float Class Double Class BigInteger BigDecimal Math Class Random Numbers Number Format Date Basics LocalDate LocalTime LocalDateTime ZonedDateTime Date Formatting Date Parsing Period Class Duration Class Java Instant Class Java Exception Basics Java try...catch Java Multiple Catch Blocks Java finally Block Java throw Keyword Java throws Keyword Java Checked Exceptions Java Unchecked Exceptions Java Custom Exceptions Java try-with-resources Java Exception Chaining
Ask about this post
AI Ask about this post

Ask questions about Abstraction and get answers drawn from it.

Signed-in readers only.