OOP - Interfaces and Abstract Classes
Learn contract-first design in Java, understand interfaces vs abstract classes, and use composition over inheritance with practical examples.
Why this step matters
As projects grow, hard-coding behavior in concrete classes quickly becomes rigid. Interfaces and abstract classes help you design extensible systems where implementation details can change without breaking calling code.
Contract-first design
A contract-first approach means defining what a component must do before deciding how it does it.
In Java, contracts are usually represented with interfaces.
public interface PaymentGateway {
void charge(String customerId, double amount);
}
Any class implementing this interface must provide charge.
Your business logic can depend on PaymentGateway instead of a specific provider.
Interfaces
Interfaces define capabilities and contracts. They are ideal when unrelated classes need to share the same behavior contract.
public interface NotificationSender {
void send(String to, String message);
}
public class EmailSender implements NotificationSender {
@Override
public void send(String to, String message) {
System.out.println("Email to " + to + ": " + message);
}
}
public class SmsSender implements NotificationSender {
@Override
public void send(String to, String message) {
System.out.println("SMS to " + to + ": " + message);
}
}
Client code can use the interface and switch implementations easily.
Abstract classes
Abstract classes are useful when you want:
- shared base state
- shared base logic
- some methods forced on subclasses
public abstract class BaseReportExporter {
protected final String format;
protected BaseReportExporter(String format) {
this.format = format;
}
public final void export(String data) {
validate(data);
write(data);
}
protected void validate(String data) {
if (data == null || data.isBlank()) {
throw new IllegalArgumentException("Data is required");
}
}
protected abstract void write(String data);
}
public class PdfExporter extends BaseReportExporter {
public PdfExporter() {
super("pdf");
}
@Override
protected void write(String data) {
System.out.println("Writing PDF: " + data);
}
}
This pattern combines reusable logic (export, validate) with subclass-specific behavior (write).
Interface vs abstract class: quick decision guide
Use an interface when:
- you need a behavior contract
- implementations may be unrelated
- you want high decoupling
Use an abstract class when:
- subclasses share state/implementation
- you need partial default behavior
- inheritance hierarchy is meaningful
In many real systems, both are used together.
Default and static methods in interfaces
Modern Java interfaces can include default and static methods.
Default method
Provides optional shared behavior that implementations may override.
public interface AuditLogger {
void log(String message);
default void logError(String message, Throwable error) {
log("[ERROR] " + message + " - " + error.getMessage());
}
}
Static method
Utility method tied to interface type.
public interface AuditLogger {
static String sanitize(String value) {
return value == null ? "" : value.trim();
}
}
Call with:
String clean = AuditLogger.sanitize(" hello ");
Composition over inheritance
Composition means building behavior by combining objects instead of extending long class hierarchies.
Why composition is often better
- easier to swap behavior at runtime
- less fragile than deep inheritance trees
- clearer responsibilities
Example with strategy-like composition
public interface TaxPolicy {
double apply(double amount);
}
public class FlatTaxPolicy implements TaxPolicy {
@Override
public double apply(double amount) {
return amount * 1.20;
}
}
public class CheckoutService {
private final TaxPolicy taxPolicy;
public CheckoutService(TaxPolicy taxPolicy) {
this.taxPolicy = taxPolicy;
}
public double total(double subtotal) {
return taxPolicy.apply(subtotal);
}
}
You can inject another TaxPolicy without changing CheckoutService.
Common mistakes to avoid
- using abstract classes by default when an interface is enough
- putting too much logic in default methods
- mixing inheritance and composition without clear intent
- leaking implementation details into contracts
Takeaway
For robust OOP design:
- start with contracts (interfaces)
- use abstract classes for shared base state/logic when truly needed
- prefer composition for behavior assembly
- keep implementation replaceable and test-friendly
These principles are foundational for clean architecture in Java applications.