Abstraction

How abstract classes and interfaces define contracts in Java — when to use each, default methods, and how abstraction enables the SOLID principles.

Last reviewed: January 2025

·Java 8Java 11Java 17Java 21
✅
Quick Understanding: Abstraction means hiding implementation details and exposing only what callers need to know. In Java, you achieve abstraction with abstract classes (partial implementation with abstract methods) and interfaces (pure contracts). When you call orderService.createOrder(), you don't know or care how it works internally — that's abstraction.

🔍 What Is It?

Abstract Class

  • →Declared with abstract keyword
  • →Can have abstract methods (no body) and concrete methods (with body)
  • →Can have instance fields and constructors
  • →Cannot be instantiated directly
  • →A subclass must implement all abstract methods (or also be abstract)

Interface

  • →Pure contract — defines what a class can do, not how
  • →All methods are implicitly public
  • →Java 8+: can have default methods (concrete) and static methods
  • →Java 9+: can have private methods (for internal use by default methods)
  • →Fields are implicitly public static final (constants)
  • →A class can implement multiple interfaces

When to use which:

  • →Abstract class: shared state + partial implementation; template method pattern; "is-a" with common behavior
  • →Interface: pure contract; multiple inheritance of type; unrelated classes sharing behavior (Serializable, Comparable, Runnable)

🎯 Why Do We Need It?

Abstraction enables:

  • →Loose coupling — callers depend on abstractions, not implementations
  • →Testability — inject mock implementations in tests
  • →Replaceability — swap implementations without changing callers
  • →Parallel development — teams work against an interface contract before implementation exists
  • →Framework integration — Spring, JPA, and all major frameworks work through interfaces

The Dependency Inversion Principle (SOLID) states: high-level modules should depend on abstractions. This is impossible without abstract classes and interfaces.

🏗️ What Happens in Real Projects?

In every Spring Boot project:

// Interface: the contract
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    List<User> findByDepartmentAndActive(String dept, boolean active);
}
// Spring Data generates the implementation at runtime — you never write it

// Interface: service contract
public interface PaymentService {
    PaymentResult process(PaymentRequest request);
    void refund(String paymentId, BigDecimal amount);
}

// Implementation (can be swapped without changing callers)
@Service
public class StripePaymentService implements PaymentService { ... }
// In tests: mock PaymentService, don't call Stripe

⚙️ How It Works

Abstract Method Resolution:

When you call an abstract method, Java uses dynamic dispatch to find the concrete implementation in the actual object's class.

Interface Default Methods:

Java 8+ allows default methods in interfaces — concrete implementations that subclasses inherit. This enables adding methods to interfaces without breaking existing implementations.

Functional Interfaces:

An interface with exactly one abstract method is a functional interface. It can be implemented with a lambda expression. @FunctionalInterface annotation enforces this.

Interface Segregation Principle:

Keep interfaces small and focused. A fat interface (20 methods) forces implementors to implement methods they don't need. Split into smaller, cohesive interfaces.

💻 Example

Abstract Classes and Interfaces in a Repository Pattern — How abstraction enables testable, swappable data access in enterprise Java.

java
// Interface: pure contract
public interface ReportGenerator {
    byte[] generate(ReportRequest request);
    String getFormat();  // "PDF", "EXCEL", "CSV"
}

// Abstract class: partial implementation with template method
public abstract class BaseReportGenerator implements ReportGenerator {
    private final AuditService auditService;
    
    protected BaseReportGenerator(AuditService auditService) {
        this.auditService = auditService;
    }
    
    // Template method — defines the algorithm
    @Override
    public final byte[] generate(ReportRequest request) {
        validateRequest(request);          // common validation
        ReportData data = fetchData(request); // abstract — subclass implements
        byte[] content = render(data);     // abstract — subclass implements
        auditService.logReport(request, getFormat()); // common audit
        return content;
    }
    
    // Common validation for all report types
    protected void validateRequest(ReportRequest request) {
        if (request.getStartDate().isAfter(request.getEndDate())) {
            throw new IllegalArgumentException("Start date must be before end date");
        }
    }
    
    // Subclasses implement these
    protected abstract ReportData fetchData(ReportRequest request);
    protected abstract byte[] render(ReportData data);
}

// Concrete implementation
@Component
public class PdfSalesReport extends BaseReportGenerator {
    private final SalesRepository salesRepo;
    
    public PdfSalesReport(AuditService audit, SalesRepository sales) {
        super(audit);
        this.salesRepo = sales;
    }
    
    @Override
    protected ReportData fetchData(ReportRequest request) {
        List<Sale> sales = salesRepo.findByDateRange(
            request.getStartDate(), request.getEndDate()
        );
        return new ReportData(sales);
    }
    
    @Override
    protected byte[] render(ReportData data) {
        // Use iText or Apache PDFBox to generate PDF
        return pdfRenderer.render(data);
    }
    
    @Override
    public String getFormat() { return "PDF"; }
}

// Another implementation — just add it, no other code changes
@Component
public class ExcelSalesReport extends BaseReportGenerator {
    @Override
    protected ReportData fetchData(ReportRequest request) { ... }
    
    @Override
    protected byte[] render(ReportData data) {
        // Use Apache POI to generate Excel
        return excelRenderer.render(data);
    }
    
    @Override
    public String getFormat() { return "EXCEL"; }
}

🤔 Common Confusions

❓ When should I use an abstract class instead of an interface?

Use abstract class when: (1) you have shared state (fields) that all subclasses need, (2) you want to provide partial implementation, (3) you are using the template method pattern. Use interface when: (1) unrelated classes need to share a contract, (2) you want multiple inheritance of type, (3) you are defining a pure API. In modern Java, interfaces with default methods cover most cases where abstract classes were previously used.

❓ What is the difference between an interface and an abstract class in Java 8+?

After Java 8, the gap narrowed significantly. Interfaces can now have default and static methods. The remaining differences: abstract classes can have instance fields and constructors; interfaces cannot. A class can implement multiple interfaces but extend only one abstract class. Abstract classes can have non-public methods; interface methods are always public (except private helper methods in Java 9+).

🚨 Common Production Issues

Fat interfaces — an interface with 20 methods forces implementors to implement all of them. Split into smaller, focused interfaces (Interface Segregation Principle).

Default method conflicts — if a class implements two interfaces with the same default method signature, it must override the method to resolve the conflict.

Leaky abstractions — an abstraction that forces callers to know implementation details defeats the purpose. Keep interfaces clean and implementation-neutral.

✅ Best Practices

Keep interfaces small and focused — Interface Segregation Principle.

Name interfaces as capabilities: Serializable, Comparable, Runnable (not ISerializable).

Use @FunctionalInterface for single-method interfaces to enable lambda usage.

Use abstract classes for template method pattern — define the algorithm in the abstract class.

Depend on the narrowest interface that satisfies your needs (List vs Collection vs Iterable).

🏛️ Architect's Note

Senior Architect's Perspective

In Spring Boot, the standard pattern is: define a service interface, implement it in a @Service class, inject the interface everywhere else. This enables: (1) unit testing with mocks, (2) swapping implementations (e.g., switch from Stripe to PayPal by swapping the @Primary service), (3) AOP proxies (Spring wraps your service in a proxy for @Transactional, @Cacheable, etc.). Without the interface, Spring can still proxy with CGLIB, but interface-based proxying is cleaner.

💬 Frequently Asked Questions

What is a marker interface?

A marker interface has no methods — it just marks a class as having some property. Examples: Serializable, Cloneable, Remote. The JVM or frameworks check instanceof to detect the marker. In modern Java, annotations (@Serializable) are preferred over marker interfaces — they are more flexible and can carry metadata.

Can interfaces have constructors?

No. Interfaces cannot be instantiated, so constructors make no sense. However, interfaces can have static factory methods (Java 8+) that return instances of anonymous or concrete implementations: List.of(), Map.of(), Optional.of() are all static factory methods on interfaces.

🎯 Key Takeaways

1Abstract classes: partial implementation + state; interfaces: pure contracts
2A class can implement multiple interfaces but extend only one class
3Java 8+ interfaces can have default and static methods
4Functional interfaces have one abstract method and can be implemented with lambdas
5Depend on abstractions (interfaces), not concrete implementations — Dependency Inversion
6Keep interfaces small and focused — Interface Segregation Principle

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these Core Java references to verify the concept in official docs and continue into the most relevant SplashRide pages.

Related SplashRide pages

SplashRide Core Java learning path

Browse the full Core Java topic collection and learning order on SplashRide.

Core Java Interview Prep

Practice Core Java interview questions and scenario-based answers.

Backend Engineer Career Path

A structured journey through Core Java, Spring Boot, production backend engineering, and interview preparation.

Full Stack Java Developer Career Path

A guided route through frontend foundations, Java backend engineering, delivery workflows, and interview readiness.

Inheritance

How Java inheritance works — extends, super, method overriding, and when to use inheritance vs composition in real enterprise projects.

Java Polymorphism: Overloading, Overriding, and Dynamic Dispatch

Learn Java polymorphism with runnable examples of overriding and overloading, declared versus runtime types, null overloads, static hiding, and interface contracts.