Abstraction in Java: Interfaces and Abstract Classes

Abstraction in Java with interfaces, abstract classes and sealed interfaces, a CSV and JSON export example, a comparison table and common FAQs.

Diagram of a ReportService that calls the ReportFormat interface, with CsvFormat, TsvFormat and JsonLinesFormat classes behind it

Abstraction in Java means that a type shows callers what it does through a small public contract, such as an interface or a set of method signatures, and keeps how it does it out of their view. The caller writes code against the contract and never depends on the class behind it.

We use abstraction to swap or change an implementation without touching the code that uses it, for example to export the same sales report as CSV today and as JSON Lines next month, or to replace a slow storage class with a faster one.

The following example defines a ReportFormat interface and one implementation that turns sales rows into CSV lines.

record Sale(String item, int units) {
}

interface ReportFormat {
    List<String> lines(List<Sale> sales);
}

class CsvFormat implements ReportFormat {
    public List<String> lines(List<Sale> sales) {
        List<String> out = new ArrayList<>();
        out.add("item,units");
        for (Sale sale : sales) {
            out.add(sale.item() + "," + sale.units());
        }
        return out;
    }
}
List<Sale> sales = List.of(new Sale("apple", 5), new Sale("pear", 3));
ReportFormat format = new CsvFormat();
List<String> csv = format.lines(sales);           // [item,units, apple,5, pear,3]
int rows = csv.size();                            // 3

Notice that the variable format has the interface type, so the next lines would work unchanged with any other ReportFormat. We cover what abstraction is, the two language tools for it (interfaces and abstract classes), how they compare, sealed interfaces for a closed set of implementations, and how abstraction differs from encapsulation.

1. What Abstraction Means in Java

Every class has two sides. The outside is the set of public methods that other code calls, and the inside is the fields and helper methods that make those methods work. Abstraction is the design decision about the outside side, namely which operations a type offers and which details it keeps to itself. It is one of the four principles in Java OOP concepts.

Diagram of a ReportService that calls the ReportFormat interface, with CsvFormat, TsvFormat and JsonLinesFormat classes behind it
The service depends only on the ReportFormat contract, so a new format needs a new class and no change in the service

The JDK is full of abstractions we use every day. The List interface offers add(), get() and size(), and our code works the same with an ArrayList backed by an array or a LinkedList backed by linked nodes. The class String offers strip() and split() without telling us how it stores characters, and the JDK changed that storage in Java 9 without breaking any caller.

Say an online store has a sales dashboard with an Export button. The first version writes CSV files. A few months later, the analytics team asks for JSON Lines, and the finance team wants tab-separated files for their spreadsheet tool. If the dashboard code calls a ReportFormat interface, each request is one new class. If the dashboard code builds CSV strings inline, each request means editing and retesting the dashboard.

2. Abstraction With Interfaces

An interface is the purest way to declare a contract in Java. It lists method signatures, and any class that implements it promises to provide them. Callers declare fields, variables, parameters and return types with the interface type, which is often called programming to an interface.

The following example adds a second format to the ReportFormat contract from the intro and a ReportService that knows nothing about CSV or JSON.

class JsonLinesFormat implements ReportFormat {
    public List<String> lines(List<Sale> sales) {
        List<String> out = new ArrayList<>();
        for (Sale sale : sales) {
            out.add("{\"item\":\"" + sale.item() + "\",\"units\":" + sale.units() + "}");
        }
        return out;
    }
}

class ReportService {
    private final ReportFormat format;

    ReportService(ReportFormat format) {
        this.format = format;
    }

    String firstLine(List<Sale> sales) {
        List<String> lines = format.lines(sales);
        return lines.isEmpty() ? "" : lines.get(0);
    }
}
List<Sale> sales = List.of(new Sale("apple", 5), new Sale("pear", 3));
ReportService csvService = new ReportService(new CsvFormat());
ReportService jsonService = new ReportService(new JsonLinesFormat());
String csvHead = csvService.firstLine(sales);     // "item,units"
String jsonHead = jsonService.firstLine(sales);   // {"item":"apple","units":5}

We can see that ReportService takes the format through its constructor. In a Spring Boot application, the container passes the right implementation, and in a unit test we pass a fake format that returns fixed lines. Both cases work because the service depends on the contract, not on a class.

3. Abstraction With Abstract Classes

An abstract class is a class that cannot be instantiated and can leave some methods without a body. It sits between an interface and a normal class. It can hold fields and constructors, plus finished methods that all subclasses share, while the abstract methods mark the parts each subclass must write.

Both CSV and tab-separated files have a header line followed by one line per sale. Only the separator differs, so the loop belongs in one place, and the subclasses supply the separator.

abstract class DelimitedFormat implements ReportFormat {
    protected abstract String separator();

    public final List<String> lines(List<Sale> sales) {
        List<String> out = new ArrayList<>();
        out.add("item" + separator() + "units");
        for (Sale sale : sales) {
            out.add(sale.item() + separator() + sale.units());
        }
        return out;
    }
}

class TsvFormat extends DelimitedFormat {
    protected String separator() {
        return "\t";
    }
}

class PipeFormat extends DelimitedFormat {
    protected String separator() {
        return "|";
    }
}
List<Sale> sales = List.of(new Sale("apple", 5), new Sale("pear", 3));
List<String> pipe = new PipeFormat().lines(sales);  // [item|units, apple|5, pear|3]
int tsvRows = new TsvFormat().lines(sales).size();     // 3

The method lines() is final, so subclasses cannot change the overall layout, only the separator. This shape is the template method pattern. Trying to create an instance of the abstract class itself is a compile error.

// does not compile: DelimitedFormat is abstract; cannot be instantiated
ReportFormat broken = new DelimitedFormat();

4. Interface or Abstract Class for Abstraction

Both tools hide implementation details, and they work well together, as the DelimitedFormat class shows by implementing ReportFormat. The choice depends on whether the shared part needs state.

QuestionInterfaceAbstract class
Can a class have several?Yes, a class implements any numberNo, a class extends only one
Instance fieldsNo, only constantsYes
ConstructorsNoYes, called by subclasses with super(…)
Method bodiesdefault, static and private methodsAny concrete method
Access of abstract methodsAlways publicpublic, protected or package-private
Typical useA contract that unrelated classes and records can implementShared code and state for a family of related classes

Start with an interface for the contract, and add an abstract class only when several implementations share fields or a fixed algorithm. The interface vs abstract class comparison covers more cases, including Java 9 private interface methods.

5. Is an Interface 100% Abstract?

Not since Java 8. Older tutorials say that interfaces give 100% abstraction and abstract classes give partial abstraction, but modern interfaces can contain code. Java 8 added default and static methods, and Java 9 added private interface methods (see JLS 9.4).

interface Exportable {
    String name();

    default String fileName() {
        return clean(name()) + ".csv";
    }

    private String clean(String raw) {
        return raw.strip().toLowerCase().replace(' ', '-');
    }
}

record MonthlyReport(String name) implements Exportable {
}
String file = new MonthlyReport(" June Sales ").fileName();  // "june-sales.csv"

An interface is still more abstract than a class in one sense, because it has no instance state. The degree of abstraction is better measured by what callers see, and callers of Exportable see two methods, whatever code sits behind them.

6. Limiting Implementations With a Sealed Interface

A normal interface is open, so any class in any module can implement it. Sometimes the set of implementations is fixed by the business, for example the two ways the dashboard delivers a report, as a download or by email. A sealed interface (Java 17) lists its permitted implementations, and a switch over it needs no default branch.

sealed interface ExportTarget permits Download, Email {
}

record Download(String fileName) implements ExportTarget {
}

record Email(String address) implements ExportTarget {
}
static String deliver(ExportTarget target) {
    return switch (target) {
        case Download d -> "saved " + d.fileName();
        case Email e -> "sent to " + e.address();
    };
}
String saved = deliver(new Download("june.csv"));      // "saved june.csv"
String mailed = deliver(new Email("ana@example.com"));  // "sent to ana@example.com"

The callers still see only the ExportTarget abstraction, while the compiler checks that every target is handled. If we add a third record to the permits list, the switch stops compiling until it handles the new case.

7. Abstraction and Encapsulation Together

Abstraction decides which operations a type offers. Encapsulation protects the data behind those operations with access modifiers, so no caller can put the object into an invalid state. In the ReportService class, the ReportFormat contract is the abstraction, and the private final field format is the encapsulation.

The two ideas often appear in the same class, which is why they get confused. A quick test helps. If the question is which methods to expose, it is abstraction, and if the question is who may read or change a field, it is encapsulation.

8. Abstraction in Java FAQs

Most of these questions come up in interviews, where the expected answer has changed since interfaces gained method bodies.

8.1. How do we achieve abstraction in Java?

We define the contract with an interface or an abstract class, and we let callers use that type instead of the concrete class. Any class with private fields and a small set of public methods is also an abstraction, so interfaces and abstract classes are the most visible tools, not the only ones.

8.2. What is a real-world example of abstraction in Java?

The JDBC API is one. Our code calls Connection and ResultSet, which are interfaces, and the driver jar for PostgreSQL or MySQL supplies the classes behind them. Switching databases means changing the driver and the URL, not the data access code.

8.3. Can we instantiate an abstract class in Java?

No, not with new. We create an instance of a concrete subclass, or of an anonymous class that implements the abstract methods on the spot.

8.4. Should every class have an interface?

No. An interface pays off when there are several implementations or when tests need a fake, such as ReportFormat. A class with one implementation and no test double, such as a small value class or a record, is already an abstraction through its public methods, and an extra interface only adds a file to maintain.

8.5. Is abstraction the same as hiding data?

No. Hiding data with private fields is encapsulation. Abstraction is about the operations a type offers, and it can exist without any fields at all, as an interface shows.

9. Conclusion

Abstraction separates what a type does from how it does it. In Java, we express the contract with interfaces, add abstract classes when several implementations share fields or a fixed algorithm, and use sealed interfaces when the set of implementations is closed.

The benefit shows up when requirements change. A caller that depends on ReportFormat gets new export formats and test doubles without a single edit. The same idea explains how the JDK can change the inside of String or HashMap between releases without breaking our code.

10. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.