Interfaces: contracts that define capability
Master Java interfaces, from abstract contracts and default methods to sealed interfaces and the interface segregation principle for clean, pluggable designs.
You build a reporting system. One class needs to be sortable. Another needs to be printable. A third needs both. You reach for inheritance, and suddenly SortablePrintableReport extends... what exactly? Java has single inheritance. You cannot extend two abstract classes. Interfaces solve this by letting a class declare multiple capabilities without locking into a single class hierarchy.
What Are Interfaces
An interface is a contract of method signatures with no implementation (mostly). It declares what a class can do without saying how. Any class that implements the interface promises to provide concrete behavior for every method in the contract.
Think of a power outlet standard. The outlet defines the shape: two prongs, specific voltage, specific amperage. Any device that matches the shape can plug in. The outlet does not care whether the device is a lamp, a blender, or a laptop charger. It only cares that the plug fits. Interfaces work the same way: they define the shape, and any class that fits the shape can be used interchangeably.
public interface Sortable<T> {
List<T> sort(List<T> data);
}
Any class implementing Sortable<T> must provide a sort() method. The caller depends on Sortable, not on BubbleSort or QuickSort. That is the entire point: callers program to the interface, not to the implementation.
Invoice implements three interfaces. No inheritance conflict. No diamond problem. Each interface is a separate capability that the class opts into independently.
For your interview: interfaces define capability ("what can this do?"), not identity ("what is this?"). A class is an Employee (inheritance), but it can be Sortable, Printable, and Payable (interfaces). That distinction matters.
Interfaces vs Abstract Classes
This comparison comes up in almost every OOP interview. The answer tells the interviewer whether you understand contracts versus partial implementations.
| Dimension | Interface | Abstract Class |
|---|---|---|
| State | No instance fields (only static final constants) | Can hold instance fields |
| Constructors | None | Yes, called via super() |
| Multiple inheritance | A class can implement many interfaces | A class can extend only one |
| Default methods | Yes (since Java 8) | Yes (always had concrete methods) |
| Access modifiers | Methods are public by default | Can be public, protected, private |
| Use when | Defining a capability contract with no shared state | Sharing partial implementation across related classes |
| Java examples | Comparable<T>, Runnable, Serializable | AbstractList, HttpServlet, InputStream |
The rule of thumb: if you need shared state or a partial template, use an abstract class. If you need a pure capability contract that multiple unrelated classes can opt into, use an interface.
I see candidates default to abstract classes when interfaces would be cleaner. The test is simple: would two completely unrelated classes (say, Invoice and Employee) both need this capability? If yes, it is an interface. Unrelated classes should never share an inheritance chain.
Interview shortcut
"I'd use an interface here because these classes share a capability, not an identity. They don't need shared state, they just need to fulfill the same contract." That sentence alone signals depth.
Default Methods
Before Java 8, adding a method to an interface broke every class that implemented it. You had a Collection interface with 50 implementations across the ecosystem. Adding stream() meant 50 classes needed updating. That is not feasible.
Default methods solve this. They let you add a method to an interface with a default implementation, so existing classes continue to compile without changes. New classes can override the default if they need different behavior.
public interface Printable {
String print();
// Added in v2 without breaking existing implementations
default String printFormatted(String format) {
return String.format(format, print());
}
}
Every class that already implements Printable gets printFormatted() for free. Classes that need custom formatting override it. This is how java.util.Collection got stream(), forEach(), and removeIf() in Java 8 without breaking the entire Java ecosystem.
The key constraint: default methods cannot access instance state (there are no instance fields in an interface). They can only call other methods defined in the same interface. This keeps them as convenience wiring, not hidden implementation.
Default methods are not abstract classes
Do not use default methods to sneak implementation into interfaces. If your interface has five default methods and two abstract ones, you have built an abstract class in disguise. Default methods exist for API evolution, not for replacing abstract classes.
Implementation
// Pure capability contract: "this thing can sort."
// No state. No constructors. Just method signatures.
// Any class, regardless of its inheritance hierarchy, can implement this.
public interface Sortable<T extends Comparable<T>> {
/** Sort the given data and return a new sorted list. */
List<T> sort(List<T> data);
/** Default: check if data is already sorted. Override for custom logic. */
default boolean isSorted(List<T> data) {
for (int i = 0; i < data.size() - 1; i++) {
if (data.get(i).compareTo(data.get(i + 1)) > 0) {
return false;
}
}
return true;
}
}Notice that PayrollRunner.runPayroll() accepts List<Payable>. It processes Invoice and Employee identically because both fulfill the Payable contract. Adding a Contractor class that implements Payable requires zero changes to PayrollRunner.
Sealed Interfaces
Java 17 introduced sealed interfaces, which restrict which classes can implement an interface. Without sealing, any class anywhere can implement your interface. That is usually fine, but sometimes you want a closed set of implementations.
// Only these three classes can implement Shape.
// The compiler enforces exhaustive pattern matching in switch expressions.
public sealed interface Shape
permits Circle, Rectangle, Triangle {
double area();
double perimeter();
}
The permits clause is the key. Only Circle, Rectangle, and Triangle can implement Shape. Any other class that tries to implement it gets a compile error.
Why does this matter? Exhaustive switch expressions. When the compiler knows every possible implementation, it can verify you handled all cases:
public String describe(Shape shape) {
return switch (shape) {
case Circle c -> "Circle with radius " + c.radius();
case Rectangle r -> "Rectangle %dx%d".formatted(r.width(), r.height());
case Triangle t -> "Triangle with base " + t.base();
// No default needed: compiler knows these are ALL the cases
};
}
If you add a Pentagon to the permits clause later, the compiler flags every switch that does not handle it. No silent bugs from missing branches.
For your interview: sealed interfaces combine the openness of interfaces (contract-based programming) with the safety of enums (exhaustive case handling). Use them when you have a known, finite set of implementations and want the compiler to enforce completeness.
Interface Segregation
The Interface Segregation Principle (ISP) says: no client should be forced to depend on methods it does not use. In practice, this means small, focused interfaces beat large, bloated ones.
Here is the problem. You have a Worker interface that covers everything an employee might do:
// Fat interface: forces every implementor to provide methods it may not need.
public interface Worker {
void code();
void attendMeeting();
void manageTeam();
void reviewCode();
void writeDocs();
}
An intern implementing Worker has to provide a body for manageTeam(). A manager has to provide a body for code(). Both are forced to implement methods irrelevant to their role. The result is empty methods, thrown exceptions, or silently wrong behavior.
The fix: split the fat interface into focused ones.
public interface Coder {
void code();
void reviewCode();
}
public interface MeetingAttendee {
void attendMeeting();
}
public interface Manager {
void manageTeam();
}
public interface DocumentWriter {
void writeDocs();
}
Now each class implements only the interfaces it actually fulfills:
public class SeniorEngineer implements Coder, MeetingAttendee, DocumentWriter { ... }
public class TeamLead implements Coder, Manager, MeetingAttendee { ... }
public class Intern implements Coder { ... }
No empty methods. No forced dependencies. Each caller depends on exactly the slice of behavior it needs. A method that only needs a Coder takes a Coder parameter, not a Worker.
The litmus test for fat interfaces
If any implementing class has a method that throws UnsupportedOperationException or has an empty body, the interface is too broad. Split it.
Common Mistakes
Fat interfaces. The most common interface mistake is cramming too many methods into one contract. The result: implementors are forced to stub out methods they do not need. Follow ISP and keep interfaces focused on a single capability.
Marker interfaces with no behavior. A marker interface (like the old Serializable) has no methods. It exists only to "tag" a class. In modern Java, annotations (@Serializable) are a better fit for this. Do not create empty interfaces to mark types unless you need compile-time type checking that annotations cannot provide.
Default method overload. Adding five default methods to an interface is a sign you want an abstract class. Default methods are for backward-compatible API evolution, not for building base implementations. If you are writing more default methods than abstract ones, reconsider the design.
Using interfaces for value types. Interfaces define capability contracts. A Point with x and y is not a capability; it is data. Use a record for value types, not an interface.
Diamond problem with defaults. If a class implements two interfaces that both define the same default method, Java refuses to compile until you override the method explicitly. This is rare in practice but catches people off guard:
interface A { default void hello() { System.out.println("A"); } }
interface B { default void hello() { System.out.println("B"); } }
// Compile error: class C inherits unrelated defaults for hello()
class C implements A, B {
@Override
public void hello() {
A.super.hello(); // Explicitly choose which default to delegate to
}
}
Test Your Understanding
Quick Recap
- An interface is a contract of method signatures that defines what a class can do, not how it does it.
- Classes can implement multiple interfaces, solving the single-inheritance limitation of abstract classes.
- Use interfaces for capability contracts (what it can do) and abstract classes for partial implementations with shared state (what it is).
- Default methods enable backward-compatible API evolution without breaking existing implementations.
- Sealed interfaces restrict which classes can implement the contract and enable exhaustive
switchexpressions. - The Interface Segregation Principle says: keep interfaces small and focused so no class is forced to implement methods it does not use.
- If any implementation throws
UnsupportedOperationException, the interface is too broad and needs splitting.
Related Concepts
- Abstraction - Interfaces are the primary mechanism for abstraction in Java. Understanding why you separate contracts from implementations makes interface design intuitive.
- Realization - The UML relationship between an interface and its implementing class. Covers the dashed-arrow notation and the "fulfills" semantics.
- Polymorphism - Interfaces enable runtime polymorphism. A method accepting
Payabledispatches toInvoice.calculatePay()orEmployee.calculatePay()based on runtime type.