An interface in Java is a contract. It defines what a class must do, but not how it does it. When a class implements an interface, it promises to provide concrete implementations for every method declared in that interface.
An interface is not a class. It cannot be instantiated, it has no constructors, and (prior to Java 8) it contained no method bodies. Think of it as a blueprint of behavior — a set of method signatures that any implementing class must fulfill.
Real-world analogy: Consider a USB port. The USB specification is an interface — it defines the shape of the connector, the voltage, the data transfer protocol. Any device manufacturer (keyboard, mouse, flash drive, phone charger) that implements the USB interface can plug into any USB port. The computer does not need to know the internal details of each device. It only needs to know that the device conforms to the USB contract.
This is exactly how Java interfaces work. Your code can depend on the interface (the contract) rather than a specific implementation, making your programs flexible, testable, and easy to extend.
// The interface defines WHAT a class must do
public interface Drawable {
void draw(); // Every Drawable must know how to draw itself
double area(); // Every Drawable must know its area
}
// Circle implements the Drawable contract -- it defines HOW
public class Circle implements Drawable {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public void draw() {
System.out.println("Drawing a circle with radius " + radius);
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
// Rectangle also implements Drawable -- with its own HOW
public class Rectangle implements Drawable {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public void draw() {
System.out.println("Drawing a rectangle " + width + "x" + height);
}
@Override
public double area() {
return width * height;
}
}
// Usage -- code depends on the interface, not the implementation
public class Main {
public static void main(String[] args) {
Drawable shape1 = new Circle(5.0);
Drawable shape2 = new Rectangle(4.0, 6.0);
shape1.draw(); // Drawing a circle with radius 5.0
shape2.draw(); // Drawing a rectangle 4.0x6.0
System.out.println("Circle area: " + shape1.area()); // 78.539...
System.out.println("Rectangle area: " + shape2.area()); // 24.0
}
}
Interfaces are one of the most powerful tools in Java. Here is why experienced developers rely on them heavily:
Interfaces let you hide implementation details. The caller only sees what operations are available, not how they work internally. This simplifies complex systems because each layer only needs to know about the contracts it depends on.
Java does not allow a class to extend more than one class (single inheritance). However, a class can implement multiple interfaces. This is Java’s solution to the multiple inheritance problem — you get the flexibility of inheriting behavior from multiple sources without the ambiguity that comes with multiple class inheritance in languages like C++.
When your code depends on an interface rather than a concrete class, you can swap implementations without changing the calling code. This is the foundation of the Dependency Injection pattern used in frameworks like Spring Boot.
This is one of the most important principles in software design (from the Gang of Four Design Patterns book). When you declare variables and parameters using interface types, your code becomes more flexible and easier to test.
Interfaces make unit testing straightforward. You can create mock or stub implementations that simulate behavior during tests, without needing a real database, network connection, or external service.
// BAD -- tightly coupled to a specific implementation
public class OrderService {
private MySqlOrderRepository repository = new MySqlOrderRepository();
public void placeOrder(Order order) {
repository.save(order); // What if we switch to PostgreSQL? We must change this class.
}
}
// GOOD -- depends on an interface (loose coupling)
public class OrderService {
private OrderRepository repository; // interface type
public OrderService(OrderRepository repository) {
this.repository = repository; // injected -- could be MySQL, Postgres, or a mock
}
public void placeOrder(Order order) {
repository.save(order); // Works with ANY implementation
}
}
// Now we can easily swap implementations or test with a mock
OrderService prodService = new OrderService(new MySqlOrderRepository());
OrderService testService = new OrderService(new InMemoryOrderRepository());
An interface is declared using the interface keyword. Here is the basic structure:
// Declaring an interface
public interface Animal {
// Constant -- automatically public, static, and final
String KINGDOM = "Animalia";
// Abstract method -- automatically public and abstract
void eat();
// Another abstract method
String speak();
// Method with a parameter and return type
boolean canFly();
}
A class implements an interface using the implements keyword. It must provide concrete implementations for every abstract method in the interface, or the class itself must be declared abstract.
public class Dog implements Animal {
@Override
public void eat() {
System.out.println("Dog eats kibble");
}
@Override
public String speak() {
return "Woof!";
}
@Override
public boolean canFly() {
return false;
}
}
public class Eagle implements Animal {
@Override
public void eat() {
System.out.println("Eagle eats fish");
}
@Override
public String speak() {
return "Screech!";
}
@Override
public boolean canFly() {
return true;
}
}
// Using the interface type for polymorphism
public class Main {
public static void main(String[] args) {
Animal myPet = new Dog();
Animal myBird = new Eagle();
System.out.println(myPet.speak()); // Woof!
System.out.println(myBird.speak()); // Screech!
System.out.println(myBird.canFly()); // true
// Accessing the constant through the interface
System.out.println(Animal.KINGDOM); // Animalia
}
}
Java interfaces follow a specific set of rules. Understanding these rules prevents common mistakes and compilation errors.
| Rule | Details |
|---|---|
Methods are public abstract by default |
You do not need to write public abstract — the compiler adds them automatically. You cannot make interface methods protected or private (abstract methods). |
Fields are public static final by default |
All fields in an interface are constants. You must initialize them at declaration. You cannot have instance variables. |
| Cannot be instantiated | You cannot write new MyInterface(). However, you can use anonymous classes or lambdas (for functional interfaces). |
| No constructors | Interfaces cannot have constructors since they cannot be instantiated. |
| A class can implement multiple interfaces | class Dog implements Animal, Pet, Trainable is valid. |
| An interface can extend multiple interfaces | interface SmartPhone extends Phone, Camera, GPS is valid. |
| Must implement all abstract methods | If a class does not implement all methods, it must be declared abstract. |
Methods can have default bodies (Java 8+) |
Use the default keyword to provide a method body in the interface. |
Methods can be static (Java 8+) |
Static methods belong to the interface, not the implementing class. |
Methods can be private (Java 9+) |
Private methods share code between default methods within the interface. |
// Demonstrating interface rules
public interface Vehicle {
// This field is automatically public, static, and final
int MAX_SPEED_MPH = 200; // Must be initialized here
// This method is automatically public and abstract
void start();
// You can write it explicitly, but it is redundant
public abstract void stop();
// INVALID -- cannot have instance variables
// int currentSpeed; // Compilation error: must be initialized (and will be static final)
// INVALID -- cannot have constructors
// Vehicle() { } // Compilation error
}
// A class implementing multiple interfaces
public interface Insurable {
double calculatePremium();
}
public interface Leasable {
double monthlyPayment(int termMonths);
}
public class Car implements Vehicle, Insurable, Leasable {
@Override
public void start() {
System.out.println("Car engine started");
}
@Override
public void stop() {
System.out.println("Car engine stopped");
}
@Override
public double calculatePremium() {
return 1200.00;
}
@Override
public double monthlyPayment(int termMonths) {
return 35000.0 / termMonths;
}
}
Before Java 8, adding a new method to an interface was a breaking change — every class that implemented the interface would fail to compile until it provided an implementation for the new method. This was a major problem for library authors. Imagine adding a new method to the List interface — every custom List implementation in the world would break.
Default methods solve this problem. They allow you to add a method with a body directly in the interface using the default keyword. Classes that implement the interface inherit the default behavior but can override it if they need to.
This is how the Java team added forEach(), stream(), and spliterator() to the Iterable and Collection interfaces in Java 8 without breaking millions of existing implementations.
public interface Logger {
// Abstract method -- must be implemented
void log(String message);
// Default method -- provides a default implementation
default void logInfo(String message) {
log("[INFO] " + message);
}
default void logWarning(String message) {
log("[WARNING] " + message);
}
default void logError(String message) {
log("[ERROR] " + message);
}
}
// ConsoleLogger only needs to implement log() -- it gets the others for free
public class ConsoleLogger implements Logger {
@Override
public void log(String message) {
System.out.println(message);
}
// logInfo, logWarning, logError are inherited with default behavior
}
// FileLogger overrides logError to add special behavior
public class FileLogger implements Logger {
@Override
public void log(String message) {
// In a real app, this would write to a file
System.out.println("[FILE] " + message);
}
@Override
public void logError(String message) {
log("[ERROR] " + message);
log("[ERROR] Stack trace saved to error.log");
// Additional error-specific logic
}
}
public class Main {
public static void main(String[] args) {
Logger console = new ConsoleLogger();
console.logInfo("Application started"); // [INFO] Application started
console.logError("Null pointer"); // [ERROR] Null pointer
Logger file = new FileLogger();
file.logInfo("Application started"); // [FILE] [INFO] Application started
file.logError("Null pointer"); // [FILE] [ERROR] Null pointer
// [FILE] [ERROR] Stack trace saved to error.log
}
}
When a class implements two interfaces that both have a default method with the same signature, you get a compilation error. Java forces you to resolve the conflict explicitly by overriding the method and choosing which version to use (or providing your own).
public interface InterfaceA {
default void greet() {
System.out.println("Hello from A");
}
}
public interface InterfaceB {
default void greet() {
System.out.println("Hello from B");
}
}
// This will NOT compile without resolving the conflict
// public class MyClass implements InterfaceA, InterfaceB { }
// Solution: override the method and resolve the conflict
public class MyClass implements InterfaceA, InterfaceB {
@Override
public void greet() {
// Option 1: Call one of the interface defaults
InterfaceA.super.greet(); // Hello from A
// Option 2: Call the other
// InterfaceB.super.greet();
// Option 3: Provide entirely new behavior
// System.out.println("Hello from MyClass");
}
}
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.greet(); // Hello from A
}
}
Java 8 also introduced static methods in interfaces. These are utility methods that belong to the interface itself, not to any implementing class. They are called using the interface name, just like static methods on a class.
Key rules for static methods in interfaces:
MyInterface.myStaticMethod()A great example from the JDK is Comparator.naturalOrder(), Comparator.reverseOrder(), and Comparator.comparing() — all static methods on the Comparator interface that create commonly needed comparators.
public interface StringUtils {
// Static utility method -- called via StringUtils.isNullOrEmpty()
static boolean isNullOrEmpty(String str) {
return str == null || str.trim().isEmpty();
}
static String capitalize(String str) {
if (isNullOrEmpty(str)) {
return str;
}
return str.substring(0, 1).toUpperCase() + str.substring(1).toLowerCase();
}
static String repeat(String str, int times) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < times; i++) {
sb.append(str);
}
return sb.toString();
}
}
// Static factory method pattern
public interface Shape {
double area();
String name();
// Factory method -- creates shapes without exposing implementation classes
static Shape circle(double radius) {
return new Shape() {
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public String name() {
return "Circle(r=" + radius + ")";
}
};
}
static Shape square(double side) {
return new Shape() {
@Override
public double area() {
return side * side;
}
@Override
public String name() {
return "Square(s=" + side + ")";
}
};
}
}
public class Main {
public static void main(String[] args) {
// Using static utility methods
System.out.println(StringUtils.isNullOrEmpty("")); // true
System.out.println(StringUtils.isNullOrEmpty("hello")); // false
System.out.println(StringUtils.capitalize("jAVA")); // Java
System.out.println(StringUtils.repeat("ha", 3)); // hahaha
// Using static factory methods
Shape c = Shape.circle(5);
Shape s = Shape.square(4);
System.out.println(c.name() + " area: " + c.area()); // Circle(r=5.0) area: 78.539...
System.out.println(s.name() + " area: " + s.area()); // Square(s=4.0) area: 16.0
}
}
Java 9 added private methods in interfaces. Their purpose is simple: they let you share code between multiple default methods within the same interface, reducing duplication.
Before Java 9, if two default methods had common logic, you had to duplicate the code or extract it into a separate utility class. Private methods keep that shared logic inside the interface where it belongs.
Rules for private methods in interfaces:
abstract -- they must have a bodyprivate (instance) or private staticprivate methods can be called from default methodsprivate static methods can be called from static methods and default methodspublic interface Reportable {
String getData();
// Default methods with shared formatting logic
default String generateHtmlReport() {
return wrapInTag("html",
wrapInTag("body",
wrapInTag("h1", "Report") +
wrapInTag("p", getData())
)
);
}
default String generateXmlReport() {
return wrapInTag("report",
wrapInTag("title", "Report") +
wrapInTag("data", getData())
);
}
// Private method -- shared by both default methods above
// Not visible to implementing classes
private String wrapInTag(String tag, String content) {
return "<" + tag + ">" + content + "" + tag + ">";
}
}
public class SalesReport implements Reportable {
@Override
public String getData() {
return "Total sales: $50,000";
}
}
public class Main {
public static void main(String[] args) {
Reportable report = new SalesReport();
System.out.println(report.generateHtmlReport());
// Report
Total sales: $50,000
System.out.println(report.generateXmlReport());
// Report Total sales: $50,000
// report.wrapInTag("div", "text"); // Compilation error -- private method
}
}
A functional interface is an interface that has exactly one abstract method. Functional interfaces are the foundation for lambda expressions and method references in Java 8+.
You can mark an interface with the @FunctionalInterface annotation. This annotation is optional but recommended -- it tells the compiler to enforce the single-abstract-method rule and gives a clear signal to other developers.
Any interface with exactly one abstract method can be used as a lambda expression target, even without the annotation. Default methods and static methods do not count toward the limit because they already have implementations.
// Custom functional interface @FunctionalInterface public interface Transformer{ T transform(T input); // Default methods are allowed -- they do not break the functional interface contract default Transformer andThen(Transformer after) { return input -> after.transform(this.transform(input)); } } public class Main { public static void main(String[] args) { // Using a lambda expression (because Transformer is a functional interface) Transformer toUpperCase = s -> s.toUpperCase(); Transformer addExclamation = s -> s + "!"; Transformer trim = s -> s.trim(); System.out.println(toUpperCase.transform("hello")); // HELLO System.out.println(addExclamation.transform("wow")); // wow! // Chaining transformations using the default method Transformer shout = trim.andThen(toUpperCase).andThen(addExclamation); System.out.println(shout.transform(" hello world ")); // HELLO WORLD! } }
Java provides a rich set of functional interfaces in the java.util.function package. You should use these before creating your own. Here are the four most important ones:
| Interface | Abstract Method | Input | Output | Use Case |
|---|---|---|---|---|
Predicate<T> |
test(T t) |
T | boolean | Filtering, validation, conditional checks |
Function<T, R> |
apply(T t) |
T | R | Transformation, mapping one type to another |
Consumer<T> |
accept(T t) |
T | void | Performing actions (logging, saving, printing) |
Supplier<T> |
get() |
none | T | Lazy creation, factory methods, providing values |
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class FunctionalInterfaceDemo {
public static void main(String[] args) {
List names = Arrays.asList("Alice", "Bob", "Charlie", "Dave", "Eve");
// Predicate -- test a condition
Predicate longerThan3 = name -> name.length() > 3;
List longNames = names.stream()
.filter(longerThan3)
.collect(Collectors.toList());
System.out.println("Long names: " + longNames); // [Alice, Charlie, Dave]
// Function -- transform data
Function nameLength = String::length;
List lengths = names.stream()
.map(nameLength)
.collect(Collectors.toList());
System.out.println("Lengths: " + lengths); // [5, 3, 7, 4, 3]
// Consumer -- perform an action
Consumer printUpperCase = name -> System.out.println(name.toUpperCase());
names.forEach(printUpperCase);
// ALICE
// BOB
// CHARLIE
// DAVE
// EVE
// Supplier -- provide a value (lazy initialization)
Supplier> defaultNames = () -> Arrays.asList("Unknown");
List result = names.isEmpty() ? defaultNames.get() : names;
System.out.println("Result: " + result); // [Alice, Bob, Charlie, Dave, Eve]
// Composing predicates
Predicate startsWithA = name -> name.startsWith("A");
Predicate longAndStartsWithA = longerThan3.and(startsWithA);
names.stream()
.filter(longAndStartsWithA)
.forEach(System.out::println); // Alice
// Chaining functions
Function toUpper = String::toUpperCase;
Function addGreeting = name -> "Hello, " + name;
Function greetLoudly = toUpper.andThen(addGreeting);
System.out.println(greetLoudly.apply("alice")); // Hello, ALICE
}
}
This is one of the most common interview questions and a fundamental design decision. Both interfaces and abstract classes define contracts that subclasses must fulfill, but they serve different purposes and have different capabilities.
| Feature | Interface | Abstract Class |
|---|---|---|
| Methods (before Java 8) | Only abstract methods | Abstract and concrete methods |
| Methods (Java 8+) | Abstract, default, static, and private (Java 9+) | Abstract and concrete methods |
| Fields | public static final only (constants) |
Any access modifier, instance and static fields |
| Constructors | No | Yes |
| Multiple inheritance | A class can implement many interfaces | A class can extend only one abstract class |
| State | Cannot hold instance state | Can hold instance state (instance variables) |
| Access modifiers on methods | Abstract methods are always public |
Methods can be public, protected, or package-private |
| When to use | To define a contract (what to do) | To define a base template with shared state/behavior |
| Keyword | implements |
extends |
| Relationship | "can do" (capability) | "is a" (identity) |
Comparable, Serializable, Runnable -- these are capabilities, not identities.AbstractList shares field management and common logic across ArrayList, LinkedList, etc.// Interface -- defines a capability
public interface Flyable {
void fly();
double maxAltitude();
}
// Abstract class -- defines a base identity with shared state
public abstract class Bird {
protected String species;
protected double wingSpan;
public Bird(String species, double wingSpan) {
this.species = species;
this.wingSpan = wingSpan;
}
// Concrete method -- shared behavior
public void breathe() {
System.out.println(species + " is breathing");
}
// Abstract method -- subclasses define their own
public abstract String habitat();
}
// Penguin IS A Bird but CANNOT fly
public class Penguin extends Bird {
public Penguin() {
super("Penguin", 0.3);
}
@Override
public String habitat() {
return "Antarctic";
}
}
// Eagle IS A Bird AND CAN fly
public class Eagle extends Bird implements Flyable {
public Eagle() {
super("Eagle", 2.0);
}
@Override
public String habitat() {
return "Mountains";
}
@Override
public void fly() {
System.out.println("Eagle soars through the sky");
}
@Override
public double maxAltitude() {
return 10000; // feet
}
}
// An Airplane is NOT a Bird but CAN fly
public class Airplane implements Flyable {
@Override
public void fly() {
System.out.println("Airplane flies with engines");
}
@Override
public double maxAltitude() {
return 40000; // feet
}
}
// This shows why interfaces represent capabilities, not identity
// Both Eagle and Airplane can fly, but they are not related by inheritance
A marker interface is an interface with no methods or constants. It is an empty interface whose sole purpose is to "mark" or "tag" a class as having a certain property. The JVM or framework checks whether a class implements the marker interface and behaves accordingly.
Well-known marker interfaces in the JDK:
java.io.Serializable -- Marks a class as safe for serialization (converting to a byte stream). The ObjectOutputStream will throw NotSerializableException if the object's class does not implement this interface.java.lang.Cloneable -- Marks a class as safe for cloning via Object.clone(). Without it, clone() throws CloneNotSupportedException.java.util.RandomAccess -- Marks a List implementation as supporting fast random access (like ArrayList). Algorithms can check this to choose between index-based or iterator-based traversal.Since Java 5, annotations have largely replaced marker interfaces for metadata tagging. Annotations are more flexible because they can carry data, be applied to methods/fields (not just classes), and do not affect the type hierarchy. However, marker interfaces still have one advantage: they define a type, which means you can use them in method signatures to enforce constraints at compile time.
import java.io.Serializable;
// Serializable is a marker interface -- no methods to implement
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String email;
public User(String name, String email) {
this.name = name;
this.email = email;
}
@Override
public String toString() {
return "User{name='" + name + "', email='" + email + "'}";
}
}
// Creating a custom marker interface
public interface Auditable {
// No methods -- just marks classes that should be audited
}
// Service that checks for the marker
public class AuditService {
public void save(Object entity) {
if (entity instanceof Auditable) {
System.out.println("AUDIT LOG: Saving " + entity.getClass().getSimpleName());
}
// ... save logic
System.out.println("Saved: " + entity);
}
}
// A class that is auditable
public class Order implements Auditable {
private String orderId;
public Order(String orderId) {
this.orderId = orderId;
}
@Override
public String toString() {
return "Order{id='" + orderId + "'}";
}
}
// A class that is NOT auditable
public class TempData {
private String value;
public TempData(String value) {
this.value = value;
}
@Override
public String toString() {
return "TempData{value='" + value + "'}";
}
}
public class Main {
public static void main(String[] args) {
AuditService auditService = new AuditService();
auditService.save(new Order("ORD-001"));
// AUDIT LOG: Saving Order
// Saved: Order{id='ORD-001'}
auditService.save(new TempData("temp"));
// Saved: TempData{value='temp'} (no audit log)
}
}
Interfaces can extend other interfaces using the extends keyword. Unlike classes, an interface can extend multiple interfaces at once. This lets you build rich type hierarchies by composing smaller, focused interfaces into larger ones.
// Small, focused interfaces (Interface Segregation Principle)
public interface Readable {
String read();
}
public interface Writable {
void write(String data);
}
public interface Closeable {
void close();
}
// Composed interface -- extends multiple interfaces
public interface ReadWriteStream extends Readable, Writable, Closeable {
// Inherits read(), write(), and close()
// Can add its own methods too
long size();
}
// A class implementing the composed interface must implement ALL methods
public class FileStream implements ReadWriteStream {
private StringBuilder buffer = new StringBuilder();
private boolean open = true;
@Override
public String read() {
if (!open) throw new IllegalStateException("Stream is closed");
return buffer.toString();
}
@Override
public void write(String data) {
if (!open) throw new IllegalStateException("Stream is closed");
buffer.append(data);
}
@Override
public void close() {
open = false;
System.out.println("Stream closed");
}
@Override
public long size() {
return buffer.length();
}
}
// You can use any level of the interface hierarchy as a type
public class Main {
public static void main(String[] args) {
ReadWriteStream stream = new FileStream();
stream.write("Hello, ");
stream.write("World!");
System.out.println(stream.read()); // Hello, World!
System.out.println(stream.size()); // 13
stream.close(); // Stream closed
// Use the narrowest type needed
Readable reader = new FileStream();
// reader.write("test"); // Compilation error -- Readable has no write()
}
}
When an interface inherits a default method from two parent interfaces, the same diamond problem applies. The sub-interface must override the conflicting method to resolve the ambiguity.
public interface Printable {
default String format() {
return "Plain text format";
}
}
public interface Exportable {
default String format() {
return "Export format";
}
}
// This interface extends both -- must resolve the conflict
public interface Document extends Printable, Exportable {
@Override
default String format() {
// Choose one, combine both, or define new behavior
return "Document: " + Printable.super.format();
}
}
public class Report implements Document {
// Inherits the resolved format() from Document
}
public class Main {
public static void main(String[] args) {
Report report = new Report();
System.out.println(report.format()); // Document: Plain text format
}
}
Interfaces are at the heart of most design patterns. Here are three patterns you will encounter frequently in professional Java codebases.
The Strategy pattern lets you define a family of algorithms, encapsulate each one behind an interface, and make them interchangeable. The client chooses which strategy to use at runtime.
// Strategy interface public interface SortStrategy> { void sort(List list); String name(); } // Concrete strategy: Bubble Sort public class BubbleSort > implements SortStrategy { @Override public void sort(List list) { int n = list.size(); for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - i - 1; j++) { if (list.get(j).compareTo(list.get(j + 1)) > 0) { T temp = list.get(j); list.set(j, list.get(j + 1)); list.set(j + 1, temp); } } } } @Override public String name() { return "Bubble Sort"; } } // Concrete strategy: Java's built-in sort public class BuiltInSort > implements SortStrategy { @Override public void sort(List list) { Collections.sort(list); } @Override public String name() { return "Built-in Sort (TimSort)"; } } // Context class that uses the strategy public class DataProcessor > { private SortStrategy strategy; public DataProcessor(SortStrategy strategy) { this.strategy = strategy; } public void setStrategy(SortStrategy strategy) { this.strategy = strategy; } public List process(List data) { System.out.println("Sorting with: " + strategy.name()); List copy = new ArrayList<>(data); strategy.sort(copy); return copy; } } // Usage public class Main { public static void main(String[] args) { List data = Arrays.asList(5, 2, 8, 1, 9, 3); DataProcessor processor = new DataProcessor<>(new BubbleSort<>()); System.out.println(processor.process(data)); // [1, 2, 3, 5, 8, 9] // Switch strategy at runtime processor.setStrategy(new BuiltInSort<>()); System.out.println(processor.process(data)); // [1, 2, 3, 5, 8, 9] } }
The Repository pattern abstracts data access behind an interface. This is the most common pattern in enterprise Java applications (Spring Boot, Jakarta EE). It separates your business logic from database-specific code.
// Domain entity
public class Product {
private Long id;
private String name;
private double price;
public Product(Long id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
// Getters
public Long getId() { return id; }
public String getName() { return name; }
public double getPrice() { return price; }
@Override
public String toString() {
return "Product{id=" + id + ", name='" + name + "', price=" + price + "}";
}
}
// Repository interface -- the contract for data access
public interface ProductRepository {
Product findById(Long id);
List findAll();
List findByPriceRange(double min, double max);
Product save(Product product);
void deleteById(Long id);
}
// Implementation for production -- talks to a real database
public class JpaProductRepository implements ProductRepository {
@Override
public Product findById(Long id) {
// In a real app: entityManager.find(Product.class, id)
System.out.println("JPA: Finding product with id " + id);
return new Product(id, "Widget", 9.99);
}
@Override
public List findAll() {
System.out.println("JPA: Querying all products from database");
return Arrays.asList(
new Product(1L, "Widget", 9.99),
new Product(2L, "Gadget", 24.99)
);
}
@Override
public List findByPriceRange(double min, double max) {
System.out.println("JPA: Querying products between $" + min + " and $" + max);
return findAll().stream()
.filter(p -> p.getPrice() >= min && p.getPrice() <= max)
.collect(Collectors.toList());
}
@Override
public Product save(Product product) {
System.out.println("JPA: Saving " + product);
return product;
}
@Override
public void deleteById(Long id) {
System.out.println("JPA: Deleting product with id " + id);
}
}
// Implementation for testing -- in-memory, no database needed
public class InMemoryProductRepository implements ProductRepository {
private Map store = new HashMap<>();
private long nextId = 1;
@Override
public Product findById(Long id) {
return store.get(id);
}
@Override
public List findAll() {
return new ArrayList<>(store.values());
}
@Override
public List findByPriceRange(double min, double max) {
return store.values().stream()
.filter(p -> p.getPrice() >= min && p.getPrice() <= max)
.collect(Collectors.toList());
}
@Override
public Product save(Product product) {
if (product.getId() == null) {
product = new Product(nextId++, product.getName(), product.getPrice());
}
store.put(product.getId(), product);
return product;
}
@Override
public void deleteById(Long id) {
store.remove(id);
}
}
In enterprise applications, the service layer sits between the controller (HTTP layer) and the repository (data layer). Defining it as an interface allows you to swap implementations, add decorators (caching, logging, transactions), and test in isolation.
// Service interface
public interface ProductService {
Product getProduct(Long id);
List getAllProducts();
List getAffordableProducts(double maxPrice);
Product createProduct(String name, double price);
}
// Production implementation
public class ProductServiceImpl implements ProductService {
private final ProductRepository repository;
public ProductServiceImpl(ProductRepository repository) {
this.repository = repository;
}
@Override
public Product getProduct(Long id) {
Product product = repository.findById(id);
if (product == null) {
throw new RuntimeException("Product not found: " + id);
}
return product;
}
@Override
public List getAllProducts() {
return repository.findAll();
}
@Override
public List getAffordableProducts(double maxPrice) {
return repository.findByPriceRange(0, maxPrice);
}
@Override
public Product createProduct(String name, double price) {
if (price < 0) throw new IllegalArgumentException("Price cannot be negative");
return repository.save(new Product(null, name, price));
}
}
// Usage -- everything wired through interfaces
public class Main {
public static void main(String[] args) {
// Production setup
ProductRepository prodRepo = new JpaProductRepository();
ProductService prodService = new ProductServiceImpl(prodRepo);
// Test setup -- swap to in-memory, no database needed
ProductRepository testRepo = new InMemoryProductRepository();
ProductService testService = new ProductServiceImpl(testRepo);
// Same code works with both
testService.createProduct("Widget", 9.99);
testService.createProduct("Gadget", 24.99);
System.out.println(testService.getAllProducts());
// [Product{id=1, name='Widget', price=9.99}, Product{id=2, name='Gadget', price=24.99}]
}
}
These guidelines come from years of building and maintaining production Java applications. Follow them to write interfaces that are clean, flexible, and easy to evolve.
The Interface Segregation Principle (the "I" in SOLID) states: no class should be forced to implement methods it does not use. If your interface has 15 methods but most implementors only need 3, break it into smaller, focused interfaces.
// BAD -- fat interface forces all implementors to deal with methods they do not need
public interface SmartDevice {
void turnOn();
void turnOff();
void setTemperature(double temp); // Not all devices have thermostats
void playMusic(String song); // Not all devices play music
void takePhoto(); // Not all devices have cameras
void makeCall(String number); // Not all devices can call
}
// GOOD -- small, focused interfaces
public interface Switchable {
void turnOn();
void turnOff();
}
public interface Thermostat {
void setTemperature(double temp);
double getTemperature();
}
public interface MusicPlayer {
void playMusic(String song);
void stop();
}
// Each class implements only what it needs
public class SmartLight implements Switchable {
@Override
public void turnOn() { System.out.println("Light on"); }
@Override
public void turnOff() { System.out.println("Light off"); }
}
public class SmartSpeaker implements Switchable, MusicPlayer {
@Override
public void turnOn() { System.out.println("Speaker on"); }
@Override
public void turnOff() { System.out.println("Speaker off"); }
@Override
public void playMusic(String song) { System.out.println("Playing: " + song); }
@Override
public void stop() { System.out.println("Music stopped"); }
}
public class SmartThermostat implements Switchable, Thermostat {
private double temp = 72.0;
@Override
public void turnOn() { System.out.println("Thermostat on"); }
@Override
public void turnOff() { System.out.println("Thermostat off"); }
@Override
public void setTemperature(double temp) { this.temp = temp; }
@Override
public double getTemperature() { return temp; }
}
Follow these established Java naming patterns:
Comparable, Serializable, Iterable, Closeable, RunnableList, Map, Set, Iterator, RepositoryIUserService). This is a C#/.NET convention, not Java. In Java, name the interface naturally (UserService) and suffix the implementation (UserServiceImpl) or name it descriptively (JpaUserService).Declare variables, parameters, and return types using the interface type, not the implementation type. This makes your code flexible and easy to change.
// BAD -- locked to a specific implementation ArrayListnames = new ArrayList<>(); HashMap scores = new HashMap<>(); // GOOD -- programmed to the interface List names = new ArrayList<>(); Map scores = new HashMap<>(); // Now you can switch to LinkedList, TreeMap, etc. without changing any other code List names = new LinkedList<>(); // No other code needs to change Map scores = new TreeMap<>(); // Automatically sorted // BAD method signature -- locked to ArrayList public ArrayList getActiveUsers() { ... } // GOOD method signature -- returns the interface type public List getActiveUsers() { ... }
When designing interfaces, think about how they will evolve over time:
| Practice | Why It Matters |
|---|---|
| Keep interfaces small and focused | Avoids forcing classes to implement unnecessary methods (ISP) |
| Use descriptive names | Makes the contract clear without reading the code |
| Declare variables using interface types | Enables swapping implementations without code changes |
| Favor interfaces over abstract classes | Allows multiple inheritance and greater flexibility |
| Add default methods for backward compatibility | Lets you evolve interfaces without breaking existing code |
Use @FunctionalInterface for single-method interfaces |
Enables lambda expressions and communicates intent |
| Document contracts with Javadoc | Helps implementors understand what is expected of them |
Let us bring everything together with a realistic example: a payment processing system. This example demonstrates interfaces, default methods, static factory methods, functional interfaces, the strategy pattern, and polymorphism -- all working together in a clean, extensible design.
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
// ============================================================
// 1. Core interface with abstract, default, static, and private methods
// ============================================================
public interface PaymentProcessor {
// Abstract methods -- every processor must implement these
boolean processPayment(double amount, String currency);
boolean refund(String transactionId, double amount);
String getProcessorName();
// Default method -- shared behavior with a sensible default
default String generateTransactionId() {
return getProcessorName().substring(0, 3).toUpperCase()
+ "-" + System.currentTimeMillis();
}
// Default method -- logging with a common format
default void logTransaction(String transactionId, double amount, String status) {
String timestamp = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println(formatLog(timestamp, transactionId, amount, status));
}
// Private method (Java 9+) -- shared formatting logic
private String formatLog(String timestamp, String txnId, double amount, String status) {
return String.format("[%s] [%s] Transaction %s: $%.2f - %s",
timestamp, getProcessorName(), txnId, amount, status);
}
// Static factory method -- creates processors without exposing implementation details
static PaymentProcessor createProcessor(String type) {
switch (type.toLowerCase()) {
case "credit_card": return new CreditCardProcessor();
case "paypal": return new PayPalProcessor();
case "crypto": return new CryptoProcessor();
default: throw new IllegalArgumentException("Unknown processor: " + type);
}
}
// Static utility method
static boolean isValidAmount(double amount) {
return amount > 0 && amount <= 1_000_000;
}
}
// ============================================================
// 2. Supplementary interface -- for processors that support subscriptions
// ============================================================
public interface SubscriptionCapable {
boolean createSubscription(String plan, double monthlyAmount);
boolean cancelSubscription(String subscriptionId);
}
// ============================================================
// 3. Functional interface -- for payment validation
// ============================================================
@FunctionalInterface
public interface PaymentValidator {
boolean validate(double amount, String currency);
// Default method to compose validators
default PaymentValidator and(PaymentValidator other) {
return (amount, currency) -> this.validate(amount, currency)
&& other.validate(amount, currency);
}
}
// ============================================================
// 4. Concrete implementations
// ============================================================
public class CreditCardProcessor implements PaymentProcessor, SubscriptionCapable {
@Override
public boolean processPayment(double amount, String currency) {
if (!PaymentProcessor.isValidAmount(amount)) {
logTransaction("INVALID", amount, "REJECTED");
return false;
}
String txnId = generateTransactionId();
// Simulate credit card processing
System.out.println("Processing credit card payment...");
System.out.println("Contacting bank for authorization...");
logTransaction(txnId, amount, "APPROVED");
return true;
}
@Override
public boolean refund(String transactionId, double amount) {
logTransaction(transactionId, amount, "REFUNDED");
return true;
}
@Override
public String getProcessorName() {
return "CreditCard";
}
@Override
public boolean createSubscription(String plan, double monthlyAmount) {
System.out.println("Credit card subscription created: " + plan
+ " at $" + monthlyAmount + "/month");
return true;
}
@Override
public boolean cancelSubscription(String subscriptionId) {
System.out.println("Subscription " + subscriptionId + " cancelled");
return true;
}
}
public class PayPalProcessor implements PaymentProcessor, SubscriptionCapable {
@Override
public boolean processPayment(double amount, String currency) {
if (!PaymentProcessor.isValidAmount(amount)) {
logTransaction("INVALID", amount, "REJECTED");
return false;
}
String txnId = generateTransactionId();
System.out.println("Redirecting to PayPal...");
System.out.println("PayPal payment confirmed.");
logTransaction(txnId, amount, "APPROVED");
return true;
}
@Override
public boolean refund(String transactionId, double amount) {
System.out.println("PayPal refund initiated (3-5 business days)");
logTransaction(transactionId, amount, "REFUNDED");
return true;
}
@Override
public String getProcessorName() {
return "PayPal";
}
@Override
public boolean createSubscription(String plan, double monthlyAmount) {
System.out.println("PayPal subscription created: " + plan);
return true;
}
@Override
public boolean cancelSubscription(String subscriptionId) {
System.out.println("PayPal subscription " + subscriptionId + " cancelled");
return true;
}
}
public class CryptoProcessor implements PaymentProcessor {
// Does NOT implement SubscriptionCapable -- crypto does not support subscriptions
@Override
public boolean processPayment(double amount, String currency) {
if (!PaymentProcessor.isValidAmount(amount)) {
logTransaction("INVALID", amount, "REJECTED");
return false;
}
String txnId = generateTransactionId();
System.out.println("Waiting for blockchain confirmation...");
logTransaction(txnId, amount, "CONFIRMED");
return true;
}
@Override
public boolean refund(String transactionId, double amount) {
System.out.println("Crypto refunds require manual wallet transfer");
logTransaction(transactionId, amount, "REFUND_PENDING");
return false;
}
@Override
public String getProcessorName() {
return "Crypto";
}
}
// ============================================================
// 5. Payment service -- ties everything together
// ============================================================
public class PaymentService {
private final List processors = new ArrayList<>();
public void registerProcessor(PaymentProcessor processor) {
processors.add(processor);
}
public boolean pay(String processorType, double amount, String currency,
PaymentValidator validator) {
// Validate first using the functional interface
if (!validator.validate(amount, currency)) {
System.out.println("Payment validation failed!");
return false;
}
// Find the right processor (programming to interface)
PaymentProcessor processor = processors.stream()
.filter(p -> p.getProcessorName().equalsIgnoreCase(processorType))
.findFirst()
.orElseThrow(() -> new RuntimeException("No processor: " + processorType));
return processor.processPayment(amount, currency);
}
// Check if a processor supports subscriptions using instanceof
public boolean subscribe(String processorType, String plan, double monthly) {
PaymentProcessor processor = processors.stream()
.filter(p -> p.getProcessorName().equalsIgnoreCase(processorType))
.findFirst()
.orElseThrow(() -> new RuntimeException("No processor: " + processorType));
if (processor instanceof SubscriptionCapable) {
return ((SubscriptionCapable) processor).createSubscription(plan, monthly);
} else {
System.out.println(processorType + " does not support subscriptions");
return false;
}
}
}
// ============================================================
// 6. Main -- running the complete example
// ============================================================
public class Main {
public static void main(String[] args) {
// --- Setup ---
PaymentService service = new PaymentService();
// Using static factory method to create processors
service.registerProcessor(PaymentProcessor.createProcessor("credit_card"));
service.registerProcessor(PaymentProcessor.createProcessor("paypal"));
service.registerProcessor(PaymentProcessor.createProcessor("crypto"));
// --- Validators using functional interfaces and lambdas ---
PaymentValidator amountCheck = (amount, currency) -> amount > 0 && amount < 10000;
PaymentValidator currencyCheck = (amount, currency) ->
Set.of("USD", "EUR", "GBP").contains(currency);
// Compose validators using the default 'and' method
PaymentValidator fullValidation = amountCheck.and(currencyCheck);
// --- Process payments ---
System.out.println("=== Credit Card Payment ===");
service.pay("CreditCard", 99.99, "USD", fullValidation);
System.out.println("\n=== PayPal Payment ===");
service.pay("PayPal", 49.50, "EUR", fullValidation);
System.out.println("\n=== Crypto Payment ===");
service.pay("Crypto", 250.00, "USD", fullValidation);
System.out.println("\n=== Invalid Payment (bad currency) ===");
service.pay("CreditCard", 50.00, "XYZ", fullValidation);
// Payment validation failed!
System.out.println("\n=== Subscriptions ===");
service.subscribe("CreditCard", "Premium", 19.99);
// Credit card subscription created: Premium at $19.99/month
service.subscribe("Crypto", "Premium", 19.99);
// Crypto does not support subscriptions
}
}
// === Sample Output ===
// === Credit Card Payment ===
// Processing credit card payment...
// Contacting bank for authorization...
// [2026-02-28 10:30:15] [CreditCard] Transaction CRE-1709125815000: $99.99 - APPROVED
//
// === PayPal Payment ===
// Redirecting to PayPal...
// PayPal payment confirmed.
// [2026-02-28 10:30:15] [PayPal] Transaction PAY-1709125815001: $49.50 - APPROVED
//
// === Crypto Payment ===
// Waiting for blockchain confirmation...
// [2026-02-28 10:30:15] [Crypto] Transaction CRY-1709125815002: $250.00 - CONFIRMED
//
// === Invalid Payment (bad currency) ===
// Payment validation failed!
//
// === Subscriptions ===
// Credit card subscription created: Premium at $19.99/month
// Crypto does not support subscriptions
| Concept | Where It Appears |
|---|---|
| Interface as a contract | PaymentProcessor defines what all processors must do |
| Multiple interface implementation | CreditCardProcessor implements PaymentProcessor, SubscriptionCapable |
| Default methods | generateTransactionId() and logTransaction() in PaymentProcessor |
| Private methods (Java 9+) | formatLog() in PaymentProcessor |
| Static methods | PaymentProcessor.createProcessor() and isValidAmount() |
| Functional interface + lambdas | PaymentValidator used with lambda expressions |
| Composing functional interfaces | amountCheck.and(currencyCheck) |
| Polymorphism | List<PaymentProcessor> holds different implementations |
| instanceof check | Checking if a processor supports SubscriptionCapable |
| Strategy pattern | Swappable processors selected at runtime |
| Factory method pattern | PaymentProcessor.createProcessor("type") |
| Programming to interfaces | All variables use interface types, not concrete classes |
| Interface Segregation | Subscription is a separate interface -- crypto does not need it |
| Loose coupling | PaymentService depends on interfaces, not implementations |
Java interfaces are one of the most important features in the language. They enable clean architecture, testable code, and flexible designs that can evolve over time. Here is what we covered:
| Topic | Key Takeaway |
|---|---|
| What is an interface | A contract that defines what a class must do, not how |
| Why use interfaces | Abstraction, loose coupling, testability, multiple inheritance |
| Syntax and rules | Methods are public abstract by default, fields are public static final |
| Default methods (Java 8+) | Add method bodies for backward compatibility |
| Static methods (Java 8+) | Utility and factory methods that belong to the interface |
| Private methods (Java 9+) | Share code between default methods without exposing it |
| Functional interfaces | Single abstract method -- enables lambdas and method references |
| Interface vs abstract class | Interfaces for capabilities, abstract classes for shared state |
| Marker interfaces | Empty interfaces that tag classes (Serializable, Cloneable) |
| Interface inheritance | Interfaces can extend multiple interfaces |
| Design patterns | Strategy, Repository, and Service Layer all rely on interfaces |
| Best practices | Keep them small, name them well, program to interfaces |
The best way to internalize these concepts is to practice. Take any class in your codebase and ask: "Would this be more flexible if it depended on an interface instead of a concrete class?" More often than not, the answer is yes.