This revised article turns the basics into a compact, practical resource that both beginners and experienced Java developers can use when designing APIs, libraries, or application code. It keeps the core facts, adds modern Java details (Java 8+ and Java 9+), shows real-world patterns, common pitfalls, and a short decision flow to pick the right tool.
1. Quick summary (the TL;DR)
- Use an abstract class when you have a shared implementation and shared state that subclasses should inherit (common fields, helper methods, constructor logic).
- Use an interface when you want to specify a contract or capability that many unrelated types may implement (multiple inheritance of type, functional APIs, mix-ins).
- Since Java 8+ interfaces can contain default and static methods; since Java 9 they can also contain private methods (to share common code between default methods).
- If you need shared mutable state or a constructor, pick an abstract class. If you need multiple behaviors or lambdas, prefer interfaces.
2. Abstract classes — what they offer, and when to pick them
What they are
An abstract class is a class that can contain abstract methods (no body) and concrete methods, can hold instance fields, and can provide constructors. It cannot be instantiated directly.
Key features (concise)
- Can contain abstract and concrete methods.
- Can declare fields of any visibility (private, protected, public).
- Can have constructors and initialization logic.
- Subclasses use
extends(single inheritance for classes). - Useful for partial implementations and shared state.
Example — shared state + behavior
abstract class Vehicle {
protected String model;
protected int year;
protected Vehicle(String model, int year) {
this.model = model;
this.year = year;
}
// abstract — subclasses must implement
public abstract void drive();
// concrete—shared behavior
public void printInfo() {
System.out.println(year + " " + model);
}
}
class Car extends Vehicle {
public Car(String model, int year) {
super(model, year);
}
@Override
public void drive() {
System.out.println("Driving the car...");
}
}
When to use an abstract class
- You need to share non-constant state (fields) or construction logic.
- You have a clear inheritance hierarchy.
- You need protected helper methods visible to subclasses.
- You want to add new non-abstract behavior later without requiring all concrete subclasses to implement it.
Limitations/pitfalls
- Single inheritance: a class can extend only one abstract class.
- Introducing new abstract methods later breaks all subclasses (unless default behavior exists).
3. Interfaces — modern capabilities and best practices
What they are
An interface defines a contract. With Java 8+, interfaces can include default and static methods; Java 9 added private methods for reuse inside the interface.
Key features (concise)
- Methods are implicitly
public(abstract methods); default and static methods may provide bodies. - Fields are
public static final(constants). - No constructors — cannot hold per-instance mutable state.
- A class may
implementsmany interfaces (multiple inheritance of type). - Interfaces are ideal for capability-driven design (e.g.,
Serializable,Comparable,Closeable, or custom capabilities likeCacheable,RateLimited).
Example — default, static, and private methods
public interface Logger {
// constant
String DEFAULT_PREFIX = "[app]";
// abstract
void log(String message);
// default — provides a shared implementation
default void logInfo(String message) {
log(DEFAULT_PREFIX + " INFO: " + message);
}
// static utility method — not tied to an instance
static String timestamp() {
return java.time.Instant.now().toString();
}
// (Java 9+) private helper used by default methods
private String decorate(String level, String msg) {
return DEFAULT_PREFIX + " " + level + ": " + msg;
}
}
Functional interfaces & lambdas
An interface with exactly one abstract method is a functional interface and can be used with lambda expressions:
@FunctionalInterface
public interface Processor<T> {
void process(T t);
}
// usage
Processor<String> p = s -> System.out.println(s.toUpperCase());
p.process("hello");
When to use an interface
- You want to define capabilities or contracts across unrelated classes.
- You want multiple inheritance of type.
- You plan to use lambdas or method references (functional interfaces).
- You want to add backward-compatible behavior via default methods.
Limitations/pitfalls
- Interfaces cannot store per-instance mutable state.
- Overusing default methods to carry stateful logic can lead to fragile designs.
- Name conflicts (diamond problem) between multiple default methods must be resolved explicitly.
4. Resolving conflicts: the diamond problem and rules
When a class implements two interfaces that define the same default method, the class must resolve the conflict:
interface A { default void hi(){ System.out.println("A"); } }
interface B { default void hi(){ System.out.println("B"); } }
class C implements A, B {
@Override
public void hi() {
// choose which to call, or define new behavior
A.super.hi(); // call A's default
// or call B.super.hi()
// or provide custom implementation
}
}
Rule summary
- Class methods always win over interface default methods.
- If two interfaces provide the same default method, the implementing class must override it (explicit resolution).
- Use
InterfaceName.super.method()inside the override to call a specific interface default.
5. Practical design guidance & patterns
API-design checklist
- If you need shared state or constructor logic → abstract class.
- If you need multiple unrelated implementations or wants to allow any type to opt-in → interface.
- If you want a single-method API for lambdas → functional interface.
- Keep interfaces small and focused — think capabilities, not monolithic APIs.
- Avoid putting mutable state in interfaces (even through weird static maps) — bad surprises for implementers and testers.
Common patterns
- Strategy: Represent interchangeable algorithms using interfaces (functional interfaces are great here).
- Template Method: Use an abstract class to provide steps with some concrete steps and some abstract methods for subclasses to fill in.
- Adapter/Decorator: Use interfaces for adapters; use abstract classes for base decorator behavior if state must be carried.
- Mixin-style features: Use interfaces with default methods to provide behavior mix-ins (e.g.,
CanLog,CanValidate).
Example: Strategy with a functional interface
@FunctionalInterface
interface SortStrategy {
<T> void sort(List<T> list, Comparator<? super T> cmp);
}
class Sorter {
private final SortStrategy strategy;
public Sorter(SortStrategy strategy) {
this.strategy = strategy;
}
public <T> void sort(List<T> list, Comparator<? super T> cmp) {
strategy.sort(list, cmp);
}
}
// usage
SortStrategy javaSort = (list, cmp) -> Collections.sort(list, cmp);
Sorter sorter = new Sorter(javaSort);
6. Advanced notes (version-sensitive)
- Java 8: introduced default and static methods in interfaces (major change to enable API evolution).
- Java 9: introduced private methods in interfaces (to avoid code duplication inside default methods).
- Java 14+ / records:
recordtypes are compact data carriers — they can implement interfaces (useful when modeling immutable DTOs). - Sealed classes (Java 15+): provide finer-grained control over class hierarchies and can be combined with interfaces to express allowed implementations and capabilities.
When designing an API intended for public use, consider Java version compatibility: default/private methods inside interfaces are not available before the version that introduced them.
7. Common pitfalls & interview-style gotchas
- Thinking default methods are stateful
Default methods cannot hold per-instance state. They execute on the implementing instance but must rely on the implementing class for any instance fields. - Forgetting visibility rules
Interface methods are implicitlypublic(abstract methods), and fields arepublic static final. Abstract class members can beprotectedorprivate. - Breaking changes
Adding a new abstract method to an abstract class or an interface (without providing a default) breaks existing implementers. Use default methods or provide an adapter/abstract base class for backward compatibility. - Overuse of marker interfaces
Marker interfaces (likeSerializable) are useful but consider annotations or dedicated metadata if the marker grows complex. - Misusing interfaces as data holders
If you need per-instance fields or constructor logic, do not use interfaces.
8. Handy cheat-sheet (single-page decision flow)
- Do you need to store instance fields or require a constructor? → Abstract class
- Do you need multiple inheritance of type (a class should adopt many behaviors)? → Interface
- Is the API a single-method functional contract? → Functional interface
- Is it OK to add default implementations for backward compatibility? → Interface (default methods)
- Do you want method visibility other than public for helper routines? → Abstract class (or Java 9+ private interface methods for inside-interface reuse)
9. Short examples comparing both in a single use-case
Use-case: Represent different payment methods (credit card, PayPal, Bank transfer). All need authorize(); some share helper utilities.
- If each payment method needs common state (e.g., tokens) and a partial shared implementation → abstract class
PaymentBase. - If payment providers are unrelated classes (one from external libs) and you only need a contract → interface
Payment.
// interface approach (flexible, no state)
public interface Payment {
boolean authorize(double amount);
}
// abstract base approach (share state)
public abstract class PaymentBase {
protected String merchantId;
protected PaymentBase(String merchantId) { this.merchantId = merchantId; }
public abstract boolean authorize(double amount);
protected boolean checkMerchant() { return merchantId != null; }
}
10. Final recommendations (practical, not dogmatic)
- Favor interfaces for capabilities and for future-proofing library users (they can implement many interfaces).
- Favor abstract classes when there is shared state or when subclassing is the natural model.
- Use default methods sparingly — they are a compatibility tool, not a replacement for careful API design.
- Document expectations (thread-safety, lifecycle, required constructor parameters) clearly whether you expose an interface or abstract class.
- Provide both an interface and an abstract helper if you want maximum flexibility: define the contract as an interface and provide an abstract base class implementation that others can extend to get sensible defaults.