Interfaces in Java

Master pure contracts: default methods, static methods, functional interfaces, and how interfaces define behavior in Java.

published: reading time: 16 min read author: Geek Workbench

Interfaces in Java

Interfaces are pure contracts — they define what a class can do without specifying how. They are the foundation of polymorphism, flexibility, and multiple inheritance in Java.

Introduction

Interfaces are pure contracts — they define what a class can do without specifying how. Where a class couples behavior to implementation, an interface decouples the contract from the implementation entirely. PaymentProcessor promises processPayment() and refundPayment(); whether that promise is fulfilled by a credit card gateway, PayPal, or Bitcoin processor is entirely up to the implementing class. This separation is the foundation of flexibility in Java design: you can swap implementations without changing the code that uses them.

The interface contract matters most in systems where behavior needs to vary at runtime or where different implementations serve different environments. A Configurable interface lets you pass database configuration in production and mock configuration in tests. An Authenticator interface lets you swap password-based auth for SSO without touching the code that depends on authentication. Interfaces enable dependency injection, plugin architectures, and the strategy pattern — all without the subclass coupling that inheritance introduces.

Java 8 expanded interfaces beyond pure contracts with default methods — implementation provided by the interface itself, inherited by all implementers. This was a pragmatic evolution: adding a new method to Collection after Java 8 would have broken every existing implementation. Default methods solve backward compatibility but introduce the diamond problem when two interfaces the same class implements both provide a default for the same method.

This post covers interface design principles, default and static methods, functional interfaces and their role in lambda expressions, sealed interfaces for controlled implementations (Java 17+), and when to prefer interfaces over abstract classes or concrete classes.

When to Use

Use interfaces when:

  • Defining a contract — specifying what unrelated classes should be able to do
  • Supporting multiple implementations — different classes with the same behavior
  • Loose coupling — depending on behavior, not concrete types
  • Dependency injection — swapping implementations without changing consumers
public interface PaymentProcessor {
    void processPayment(double amount);

    boolean refundPayment(String transactionId);
}

public class CreditCardProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        // Credit card specific logic
        System.out.println("Processing credit card payment: $" + amount);
    }

    @Override
    public boolean refundPayment(String transactionId) {
        // Refund logic
        return true;
    }
}

public class PayPalProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        // PayPal specific logic
        System.out.println("Processing PayPal payment: $" + amount);
    }

    @Override
    public boolean refundPayment(String transactionId) {
        // Refund logic
        return true;
    }
}

// Usage — depends on interface, not concrete type
public class OrderService {
    private final PaymentProcessor processor;

    public OrderService(PaymentProcessor processor) {
        this.processor = processor;
    }

    public void checkout(double amount) {
        processor.processPayment(amount);
    }
}

When Not to Use

Avoid interfaces when:

  • Only one implementation exists — creating an interface for a single class adds indirection without benefit
  • You need shared state — interfaces cannot have instance fields
  • The contract is tightly coupled to implementation — a class is more appropriate
  • You need protected methods — interfaces only have public and static methods
// Over-engineering: interface for single implementation
public interface Greeter { void greet(); }

public class HelloGreeter implements Greeter {
    @Override
    public void greet() { System.out.println("Hello!"); }
}

// Better: just use the class directly
public class HelloGreeter {
    public void greet() { System.out.println("Hello!"); }
}

Interface Hierarchy — Mermaid Diagram

classDiagram
    interface PaymentProcessor {
        <<interface>>
        +processPayment(amount) void
        +refundPayment(transactionId) boolean
    }
    interface Refundable {
        <<interface>>
        +refundPayment(transactionId) boolean
    }
    interface Completable {
        <<interface>>
        +complete() void
    }
    class CreditCardProcessor {
        +processPayment(amount) void
        +refundPayment(transactionId) boolean
    }
    class BitPayProcessor {
        +processPayment(amount) void
        +complete() void
    }
    PaymentProcessor <|.. CreditCardProcessor
    PaymentProcessor <|.. Refundable
    Refundable <|.. BitPayProcessor
    Completable <|.. BitPayProcessor
    note for CreditCardProcessor "implements PaymentProcessor\nand implicitly implements Refundable"

Failure Scenarios

1. Default Method Collision (Diamond Problem)

public interface A {
    default void execute() { System.out.println("A"); }
}

public interface B {
    default void execute() { System.out.println("B"); }
}

// COMPILE ERROR — both interfaces have same default method
public class C implements A, B {
    // Must override to resolve conflict
    @Override
    public void execute() {
        // Call one or both
        A.super.execute();  // Explicitly call A's version
        B.super.execute();  // Or B's version
    }
}

2. Adding Methods to Existing Interfaces (Breaking Change)

public interface V1DataStore {
    Object get(String key);
    void put(String key, Object value);
}

// Later — need new method
public interface V2DataStore {
    Object get(String key);
    void put(String key, Object value);
    void delete(String key);  // New method
}

// All existing implementations BREAK — they don't implement delete()
// Fix: default method
public interface V2DataStore {
    Object get(String key);
    void put(String key, Object value);
    default void delete(String key) {  // Default implementation
        throw new UnsupportedOperationException("Not implemented");
    }
}

3. Confusing Interface with Implementation

public interface Validator<T> {
    boolean isValid(T value);  // Just the contract
}

// Wrong: adding implementation to interface
public interface Validator<T> {
    boolean isValid(T value);
    default boolean isValidOrThrow(T value) {  // Coupling behavior to contract
        if (!isValid(value)) throw new IllegalArgumentException("Invalid");
        return true;
    }
}

// Better: separate concerns
public interface Validator<T> {
    boolean isValid(T value);
}

public class ValidationUtils {
    private ValidationUtils() { }  // Utility class
    public static <T> void requireValid(T value, Validator<T> validator) {
        if (!validator.isValid(value)) throw new IllegalArgumentException("Invalid");
    }
}

Trade-off Table

Interface FeatureUse WhenLimitation
Abstract methodsStandard contract — all implementers provide own behaviorNone
Default methodsAdding methods without breaking existing implementersMultiple default collision
Static methodsFactory methods, utility functions tied to interfaceNot inherited by implementing classes
Private methods (Java 9+)Sharing code between default methodsCannot be called externally
Constant fieldsOnly for related constants — prefer enumsImplicitly public static final

Code Snippets

Default Methods

public interface Readable {
    String read();

    // Default implementation for common behavior
    default boolean isEmpty() {
        return read().isEmpty();
    }

    // Chaining default methods
    default String readTrimmed() {
        return read().trim();
    }
}

public class FileReader implements Readable {
    private final String path;

    @Override
    public String read() {
        return Files.readString(Path.of(path));
    }

    // Inherits isEmpty() and readTrimmed() from interface
}

Functional Interface

// A functional interface has exactly one abstract method
@FunctionalInterface
public interface Transformer<T, R> {
    R transform(T input);

    // Can have default methods too
    default Transformer<T, R> andThen(Transformer<R, ?> after) {
        return t -> after.transform(transform(t));
    }
}

// Usage with lambda
List<Integer> numbers = List.of(1, 2, 3);
List<String> strings = numbers.stream()
    .map(n -> n * 2)  // Transformer<Integer, Integer>
    .map(String::valueOf)  // Transformer<Integer, String>
    .toList();

Interface as Type

public interface Configurable {
    void configure(Map<String, String> settings);
}

// Methods can accept interface type
public class ServiceRunner {
    public void run(Configurable configurable) {
        Map<String, String> defaultSettings = Map.of("mode", "production");
        configurable.configure(defaultSettings);
    }
}

// Any class implementing Configurable can be used
public class DatabaseConfig implements Configurable {
    @Override
    public void configure(Map<String, String> settings) {
        // Database specific configuration
    }
}

Observability Checklist

  • Interface names describe capability (e.g., Readable, Writable, Serializable)
  • Only one responsibility per interface — smaller interfaces over large ones
  • Default methods provide backward compatibility, not just convenience
  • @FunctionalInterface annotation on single-abstract-method interfaces
  • Consider sealed interfaces (Java 17+) to control implementers

Security Notes

  • Default methods can be circumvented — implementers can override default methods
  • Constant fields exposed — interface constants are public static final, accessible to anyone
  • Don’t put sensitive logic in interfaces — interface is contract, not implementation
  • Sealed interfaces — restrict which classes can implement your interface (Java 17+)
// Sealed interface — control implementations
public sealed interface Shape permits Circle, Square, Triangle {
    double area();
}

public final class Circle implements Shape {
    private final double radius;
    public Circle(double radius) { this.radius = radius; }
    @Override public double area() { return Math.PI * radius * radius; }
}

public final class Square implements Shape {
    private final double side;
    public Square(double side) { this.side = side; }
    @Override public double area() { return side * side; }
}

public final class Triangle implements Shape {
    private final double base, height;
    public Triangle(double base, double height) { this.base = base; this.height = height; }
    @Override public double area() { return 0.5 * base * height; }
}

// No other class can implement Shape — compiler enforces allowed types

Pitfalls

  1. Too many methods — split large interfaces (Interface Segregation Principle)
  2. Default method for shared behavior — may couple implementations unnecessarily
  3. Adding abstract methods breaking existing code — use default methods instead
  4. Confusing interface inheritance with implementation — interface never has state
  5. Excessive interface inheritance chains — shallow hierarchies are clearer
// Bad: interface with too many responsibilities
public interface Vehicle {
    void start();
    void stop();
    void accelerate(double amount);
    void brake(double amount);
    void openDoors();
    void refuel(double amount);
    // Too much — split into smaller interfaces
}

// Good: small, focused interfaces
public interface Startable { void start(); }
public interface Stoppable { void stop(); }
public interface Acceleratable { void accelerate(double amount); }
public interface Refuelable { void refuel(double amount); }

Quick Recap

  • Interface = pure contract, no state (until Java 8 default methods)
  • implements = keyword for providing interface implementations
  • Default method = default keyword provides implementation; enables interface evolution
  • Static method = belongs to interface, not implementing class; good for factories
  • Functional interface = exactly one abstract method; can use lambda expressions
  • @FunctionalInterface = compiler check that interface is valid for lambdas

Interview Questions

1. What is the difference between a class and an interface?

Model Answer: "A class can have state (fields) and fully implemented methods. An interface can only declare abstract methods (pre-Java 8) or have default/static methods (Java 8+). A class can only extend one class but implement many interfaces. Use class for implementation; use interface for contracts."

2. What are default methods and why were they introduced?

Model Answer: "Default methods are methods in an interface with a body, declared using the `default` keyword. They were introduced in Java 8 to allow adding new methods to interfaces without breaking existing implementations. Existing classes automatically inherit default implementations without modification."

3. Can an interface have a constructor?

Model Answer: "No, interfaces cannot have constructors because they cannot be instantiated. They can have static methods with implementation (Java 8+) and private methods (Java 9+) for internal code sharing between default methods."

4. What is a functional interface?

Model Answer: "A functional interface is an interface with exactly one abstract method. It can have multiple default or static methods. The `@FunctionalInterface` annotation triggers compiler checking. Examples include `Runnable`, `Callable`, `Comparator`. These can be represented as lambda expressions."

5. What happens when two interfaces a class implements have conflicting default methods?

Model Answer: "The class must explicitly override the conflicting method and decide which default to call (e.g., `InterfaceA.super.method()`) or provide its own implementation. If the class doesn't override, compilation fails. This is the only form of multiple inheritance in Java — calling specific default implementations from different interfaces."

6. What is the difference between implementing multiple interfaces and extending a class?

Model Answer: "A class can implement many interfaces but can only extend one class (single inheritance). Interfaces provide contracts without state; classes provide both contracts and state. Multiple interfaces compose contracts without the diamond problem (no state inheritance)."

7. What are the rules for default method collision resolution in Java?

Model Answer: "Class wins over interface — implementing class's method takes priority. If class doesn't override, choose most specific interface (more specific extends another). If two unrelated interfaces both have default, class must explicitly override the method."

8. What is the purpose of static methods in interfaces since Java 8?

Model Answer: "Static methods belong to interface, not implementing class — similar to utility methods. Common use: factory methods returning implementing instances (like `List.of()`). Static interface methods are not inherited by implementing classes."

9. Why were default methods introduced in Java 8?

Model Answer: "To allow adding new methods to existing interfaces without breaking existing implementations. Enabled extension of core interfaces (like Collection) without forcing all implementations to add code. Backward compatibility was the main driver — adding new abstract methods would break older code."

10. What is the difference between extends and implements in interface declaration?

Model Answer: "Interface extends another interface — inherits its abstract methods (and default methods if any). Interface cannot implement — only classes implement interfaces. Class implements interface(s) — provides implementations for abstract methods."

11. Can an interface have private methods since Java 9?

Model Answer: "Yes — private interface methods allow sharing code between default methods without exposing it. Private methods cannot be abstract — they must have implementation. Used to avoid code duplication in complex default methods."

12. What is the relationship between functional interfaces and lambda expressions?

Model Answer: "Functional interface = exactly one abstract method → can be represented as lambda expression. Lambda provides concise syntax for implementing functional interface on the spot. Type of lambda is inferred from context — compiler matches to target functional interface."

13. What is the difference between Runnable and Callable in Java?

Model Answer: "Runnable: functional interface with run() method, returns no value, cannot throw checked exceptions. Callable: functional interface with call() method, returns V, can throw checked exceptions. ExecutorService.submit(Runnable) returns Future, submit(Callable) returns Future."

14. What is marker interface and give examples in Java?

Model Answer: "Marker interface has no methods — its presence marks a class for special treatment. Examples: Serializable (marks class as serializable), Cloneable (enables Object.clone()). Alternative: annotations provide more flexible metadata without interface contract."

15. What is the difference between interface constant and enum for fixed values?

Model Answer: "Interface constant is public static final field — accessible anywhere via InterfaceName.CONSTANT. Enum is a proper type with type safety — cannot be mistaken for other ints. Enum provides better organization with related constants grouped as type; preferred for fixed sets."

16. What is the purpose of the @FunctionalInterface annotation?

Model Answer: "Compiler checking — ensures interface really has exactly one abstract method. If you add second abstract method, compilation fails — catches accidental additions. Documents intent for tool generation (lambdas, method references) and code readability."

17. Can interfaces have abstract fields? What is the default?

Model Answer: "Interface fields are implicitly public static final — they are constants. Cannot be abstract (what would abstract constant mean?) — all fields are assigned values. Interface fields are essentially static constants: public static final TYPE NAME = value;"

18. What is the difference between interface and abstract class for API design?

Model Answer: "Interface defines contract without implementation — for APIs where multiple implementations exist. Abstract class provides skeletal implementation with shared code — for class hierarchies with code reuse. Both are API contract mechanisms — choose based on whether you need shared implementation."

19. Can a class implement two interfaces that both have a default method of the same name?

Model Answer: "Yes, the class must override the conflicting default method. In the override, class can explicitly call one of: InterfaceA.super.method() or InterfaceB.super.method(). If class doesn't override, compilation error occurs due to ambiguous inheritance."

20. What is the impact of interface evolution on existing code with default methods?

Model Answer: "Default methods allow adding new methods without breaking existing implementations. Existing implementations automatically inherit the default behavior. Risk: adding new default methods with same signature as existing implementations causes diamond problem."

Further Reading

Conclusion

Interfaces define pure contracts — they specify what a class can do without caring how. A PaymentProcessor promises processPayment() and refundPayment(), but the implementation details (credit card vs PayPal) are entirely up to the class that implements the interface. This separation of “what” from “how” is what makes interfaces the foundation of flexible, loosely-coupled Java design.

The interface hierarchy diagram shows how interfaces can extend each other, forming chains of contracts. A Refundable interface that extends PaymentProcessor means any class implementing Refundable must implement both interfaces’ methods. This compositional approach to contracts — building rich interfaces from smaller ones — is more flexible than having one monolithic interface.

Default methods (Java 8) changed interfaces from pure abstract contracts to having implementation too. They solve a real problem: adding a new method to an interface breaks all existing implementations. With a default method, existing implementations inherit the default behavior without modification. But default methods come with the diamond problem — if two interfaces the same class implements both have a default method with the same signature, the class must resolve the conflict explicitly.

Functional interfaces — marked with @FunctionalInterface — have exactly one abstract method and can be represented as lambda expressions. This connects interfaces to the Strategy and Command patterns, where behavior is passed around as objects. Instead of defining a class for each small behavior, you write a lambda: list.stream().map(s -> s.toUpperCase()) is a Transformer<String, String> implemented as a lambda.

Interfaces complement abstract classes (detailed in Abstract Classes in Java) and composition (covered in Composition over Inheritance). Where abstract classes provide shared implementation, interfaces provide pure contracts. Where inheritance creates tight coupling via “is-a”, composition with interface-typed collaborators creates loose coupling — you can swap FileLogger for ConsoleLogger at runtime without changing the code that uses them.

Category

Related Posts

Abstract Classes in Java

Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.

#java-abstract-classes #java #java-fundamentals

Arithmetic Operators in Java

Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.

#java-arithmetic-operators #java #java-fundamentals

Array Basics in Java

Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.

#java-array-basics #java #java-fundamentals