Abstraction
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 Reportsays: "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 callsbody(). When it runs on aSalesReport, theSalesReportversion ofbody()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
abstractitself. - An abstract method cannot be
private(subclasses could not implement it),static(static methods are not overridden), orfinal(it must be overridden). - An abstract class may have no abstract methods; marking it
abstractsimply 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
| Member | Since | Notes |
|---|---|---|
| Abstract methods | Java 1.0 | Implicitly public abstract |
| Constants | Java 1.0 | Implicitly public static final |
default methods | Java 8 | Have a body; implementing classes inherit or override them |
static methods | Java 8 | Utility methods called on the interface name |
private methods | Java 9 | Helpers 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 class | Interface | |
|---|---|---|
| Instance fields (state) | Yes | No (constants only) |
| Constructors | Yes | No |
| Method implementations | Any | default, static, private only |
| A class can extend/implement | One | Many |
| Access levels for methods | Any | public (or private helpers) |
| Expresses | "is a kind of", with shared code and state | "can do", a capability or contract |
| Typical examples | AbstractList, HttpServlet, InputStream | List, 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
ListandAbstractListfollow 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:
| Abstraction | Encapsulation | |
|---|---|---|
| Focus | What the outside world sees: the essential operations | Protecting internal data and enforcing rules |
| Main tools | Interfaces, abstract classes, well-chosen method names | private 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.