Skip to main content
Java intermediate Lesson 19 of 58

Interfaces in Java — A Deep Dive

Go beyond the basics — learn interface segregation, default methods, functional interfaces, and how interfaces enable real-world design patterns.

Interfaces are Java’s mechanism for defining contracts without implementation. At the intermediate level you move from just implementing interfaces to designing them — knowing when to split them, when to add defaults, and how to use them as the foundation for flexible, testable code.

Interface Anatomy (Java 17)

An interface can contain several kinds of members. Abstract methods define the contract that every implementing class must fulfill. Default methods provide a shared fallback behavior so you can add new methods to an existing interface without breaking all its implementors. Static utility methods belong to the interface itself and are useful for helpers tightly related to the contract. Constants on interfaces are implicitly public static final.

public interface PaymentGateway {

    // 1. Abstract methods — implementors must provide these
    String charge(String customerId, double amount, String currency);
    boolean refund(String chargeId, double amount);
    PaymentStatus getStatus(String chargeId);

    // 2. Default method — fallback; can be overridden by any implementor
    default boolean fullRefund(String chargeId) {
        return refund(chargeId, -1); // signal "full" to implementation
    }

    // 3. Static utility method — belongs to the interface, not instances
    static String formatAmount(double amount, String currency) {
        return String.format("%.2f %s", amount, currency.toUpperCase());
    }

    // 4. Constant — implicitly public static final
    int MAX_RETRY_ATTEMPTS = 3;
}

Interface Segregation in Practice

A common mistake is creating one large interface that mixes unrelated concerns. When a class must implement methods it doesn’t need, any change to those irrelevant methods forces a recompile and a stub update everywhere. Keeping interfaces small and focused means each implementor only depends on methods it actually uses, and you can swap out one capability independently of others.

// BAD — a god interface that mixes persistence, email, and export concerns
public interface UserService {
    User findById(long id);
    void save(User user);
    void delete(long id);
    void sendWelcomeEmail(User user);      // unrelated to persistence
    void sendPasswordReset(User user);     // unrelated to persistence
    byte[] exportToCsv();                  // unrelated to email
    void importFromCsv(byte[] data);
}

// GOOD — each interface has a single, coherent responsibility
public interface UserRepository {
    User findById(long id);
    void save(User user);
    void delete(long id);
}

public interface UserNotifier {
    void sendWelcomeEmail(User user);
    void sendPasswordReset(User user);
}

public interface UserExporter {
    byte[] exportToCsv();
    void importFromCsv(byte[] data);
}

// A full UserService can compose all three — or you can inject each separately
public class FullUserService implements UserRepository, UserNotifier, UserExporter {
    // ... implements only what it owns
}

Interface Inheritance

Interfaces can extend other interfaces, letting you build layered capability hierarchies. This is especially useful for building richer contracts out of simpler ones, while allowing classes to implement only the level they support.

public interface Readable {
    String read(String path);
}

public interface Writable {
    void write(String path, String content);
}

// Combines both capabilities and adds a default convenience method
public interface ReadWritable extends Readable, Writable {
    default void copyTo(String from, String to) {
        write(to, read(from)); // reuses the abstract methods
    }
}

public class LocalFileSystem implements ReadWritable {
    @Override
    public String read(String path) {
        return "contents of " + path; // simplified
    }

    @Override
    public void write(String path, String content) {
        System.out.println("Writing to " + path + ": " + content);
    }
}

ReadWritable fs = new LocalFileSystem();
fs.copyTo("/tmp/source.txt", "/tmp/dest.txt");

Functional Interfaces and Lambdas

A @FunctionalInterface has exactly one abstract method, which lets you express it as a lambda or method reference. This is the bridge between Java’s OOP model and functional-style programming. Composable functional interfaces let you build processing pipelines without creating a new class for each step.

@FunctionalInterface
public interface Transformer<T, R> {
    R transform(T input);

    // Default methods for pipeline composition — don't count toward the one-abstract-method rule
    default <V> Transformer<T, V> andThen(Transformer<R, V> after) {
        return input -> after.transform(this.transform(input));
    }
}

// Lambdas and method references are valid Transformer implementations
Transformer<String, String>  trim     = String::trim;
Transformer<String, String>  lower    = String::toLowerCase;
Transformer<String, Integer> length   = String::length;

// Compose into a pipeline using andThen
Transformer<String, Integer> pipeline = trim.andThen(lower).andThen(length);

System.out.println(pipeline.transform("  HELLO WORLD  ")); // 11

Using Interfaces for Strategy Pattern

Interfaces are the backbone of the Strategy pattern. By defining an algorithm contract as an interface, you can swap the implementation at runtime without touching the code that uses it. This is far cleaner than a chain of if/else blocks that grows every time you add a variant.

public interface SortStrategy<T extends Comparable<T>> {
    void sort(List<T> list);
    default String name() { return getClass().getSimpleName(); }
}

// Each strategy encapsulates a different algorithm
public class BubbleSort<T extends Comparable<T>> implements SortStrategy<T> {
    @Override
    public void sort(List<T> 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);
                }
            }
        }
    }
}

public class JavaBuiltinSort<T extends Comparable<T>> implements SortStrategy<T> {
    @Override
    public void sort(List<T> list) { Collections.sort(list); }
}

// The Sorter class never needs to change when new strategies are added
public class Sorter<T extends Comparable<T>> {
    private SortStrategy<T> strategy;

    public Sorter(SortStrategy<T> strategy) { this.strategy = strategy; }

    public void setStrategy(SortStrategy<T> strategy) { this.strategy = strategy; }

    public List<T> sort(List<T> data) {
        List<T> copy = new ArrayList<>(data);
        strategy.sort(copy);
        System.out.println("Sorted with: " + strategy.name());
        return copy;
    }
}

List<Integer> numbers = List.of(5, 2, 8, 1, 9, 3);
Sorter<Integer> sorter = new Sorter<>(new BubbleSort<>());
System.out.println(sorter.sort(numbers)); // [1, 2, 3, 5, 8, 9]

// Switch algorithm without changing Sorter
sorter.setStrategy(new JavaBuiltinSort<>());
System.out.println(sorter.sort(numbers)); // [1, 2, 3, 5, 8, 9]

Marker Interfaces

A marker interface has no methods at all. Its purpose is to tag a class as having a certain property so that other code can make decisions based on it. Java’s own Serializable and Cloneable are classic examples. This pattern is less common today — annotations often serve the same purpose — but you still encounter it in legacy and framework code.

// Built-in examples: java.io.Serializable, java.lang.Cloneable

public interface Exportable {} // marker — signals that this object can be exported

public class Report implements Exportable {
    private String title;
    private String content;
    // ...
}

public class ExportService {
    public void export(Object obj) {
        // The marker is checked at runtime via instanceof
        if (!(obj instanceof Exportable)) {
            throw new IllegalArgumentException(obj.getClass().getSimpleName() + " is not exportable.");
        }
        System.out.println("Exporting: " + obj.getClass().getSimpleName());
    }
}

Testing with Interfaces

One of the biggest practical benefits of interfaces is testability. When a class depends on an interface rather than a concrete class, you can supply a lightweight test double instead of the real implementation — no real SMTP server, no real database. This makes tests fast, deterministic, and free of external side effects.

public interface EmailService {
    void send(String to, String subject, String body);
}

// Production implementation — uses real SMTP
public class SmtpEmailService implements EmailService {
    @Override
    public void send(String to, String subject, String body) {
        // real SMTP logic
    }
}

// Test double — records calls so tests can assert on them
public class MockEmailService implements EmailService {
    private final List<String> sentTo = new ArrayList<>();

    @Override
    public void send(String to, String subject, String body) {
        sentTo.add(to); // capture the call instead of sending
    }

    public boolean wasSentTo(String email) { return sentTo.contains(email); }
    public int getSentCount()              { return sentTo.size(); }
}

// In your test — no SMTP server needed
MockEmailService mockEmail = new MockEmailService();
UserRegistrationService service = new UserRegistrationService(mockEmail);
service.register("alice@example.com");

assert mockEmail.wasSentTo("alice@example.com");
assert mockEmail.getSentCount() == 1;

Frequently Asked Questions

Can an interface extend another interface?
Yes. An interface can extend one or more other interfaces using the extends keyword. A class implementing the child interface must implement all methods from both the child and parent interfaces.
What is the difference between a default method and an abstract method in an interface?
An abstract method has no body — implementing classes must provide one. A default method has a body and acts as a fallback that implementing classes can optionally override.
What is interface segregation?
Interface segregation is the 'I' in SOLID. It means keeping interfaces small and focused. A class should not be forced to implement methods it does not need. Prefer many small interfaces over one large one.