1. java
  2. /oop
  3. /interfaces

Master Java Interfaces and Abstract Programming

Java Interfaces

Interfaces in Java define contracts that classes must follow. They represent pure abstraction by specifying what a class must do without defining how it should be done. Interfaces enable multiple inheritance of type and provide a way to achieve loose coupling in object-oriented design.

What is an Interface?

An interface is a reference type that contains:

  • Abstract methods (by default)
  • Default methods (Java 8+)
  • Static methods (Java 8+)
  • Constants (public, static, final by default)
  • Private methods (Java 9+)
public interface Drawable {
    // Constant (implicitly public, static, final)
    int MAX_OBJECTS = 1000;
    
    // Abstract method (implicitly public and abstract)
    void draw();
    void setColor(String color);
    
    // Default method (Java 8+)
    default void highlight() {
        System.out.println("Highlighting the drawable object");
    }
    
    // Static method (Java 8+)
    static void printInfo() {
        System.out.println("Drawable interface for rendering objects");
    }
    
    // Private method (Java 9+)
    private void validateColor(String color) {
        if (color == null || color.isEmpty()) {
            throw new IllegalArgumentException("Color cannot be null or empty");
        }
    }
}

Implementing Interfaces

Single Interface Implementation

public class Circle implements Drawable {
    private String color;
    private double radius;
    
    public Circle(double radius) {
        this.radius = radius;
        this.color = "black"; // default color
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing a " + color + " circle with radius " + radius);
    }
    
    @Override
    public void setColor(String color) {
        this.color = color;
    }
    
    // Can call default method without overriding
    public void showCircle() {
        draw();
        highlight(); // Uses default implementation
    }
}

// Usage
Circle circle = new Circle(5.0);
circle.draw();
circle.setColor("red");
circle.highlight(); // Default method from interface
Drawable.printInfo(); // Static method from interface

Multiple Interface Implementation

public interface Moveable {
    void move(double x, double y);
    void stop();
    
    default void resetPosition() {
        move(0, 0);
        System.out.println("Position reset to origin");
    }
}

public interface Scalable {
    void scale(double factor);
    double getScale();
    
    default void resetScale() {
        scale(1.0);
        System.out.println("Scale reset to 1.0");
    }
}

// Class implementing multiple interfaces
public class Shape implements Drawable, Moveable, Scalable {
    private String color;
    private double x, y;
    private double scale;
    
    public Shape() {
        this.color = "black";
        this.x = 0;
        this.y = 0;
        this.scale = 1.0;
    }
    
    // Implementing Drawable
    @Override
    public void draw() {
        System.out.println("Drawing " + color + " shape at (" + x + ", " + y + 
                          ") with scale " + scale);
    }
    
    @Override
    public void setColor(String color) {
        this.color = color;
    }
    
    // Implementing Moveable
    @Override
    public void move(double x, double y) {
        this.x = x;
        this.y = y;
        System.out.println("Moved to (" + x + ", " + y + ")");
    }
    
    @Override
    public void stop() {
        System.out.println("Shape stopped at (" + x + ", " + y + ")");
    }
    
    // Implementing Scalable
    @Override
    public void scale(double factor) {
        this.scale = factor;
        System.out.println("Scaled to " + factor);
    }
    
    @Override
    public double getScale() {
        return scale;
    }
}

Default Methods

Default methods provide implementations in interfaces (Java 8+):

Basic Default Methods

public interface Vehicle {
    void start();
    void stop();
    
    // Default method with implementation
    default void honk() {
        System.out.println("Beep beep!");
    }
    
    default void displayStatus() {
        System.out.println("Vehicle is ready");
    }
    
    // Default method can call other interface methods
    default void startAndHonk() {
        start();
        honk();
    }
}

public class Car implements Vehicle {
    private boolean isRunning = false;
    
    @Override
    public void start() {
        isRunning = true;
        System.out.println("Car engine started");
    }
    
    @Override
    public void stop() {
        isRunning = false;
        System.out.println("Car engine stopped");
    }
    
    // Can override default method if needed
    @Override
    public void honk() {
        System.out.println("Car horn: HONK HONK!");
    }
    
    // Uses default implementation of displayStatus()
}

Default Method Conflicts

When multiple interfaces have the same default method:

public interface Interface1 {
    default void method() {
        System.out.println("Interface1 default method");
    }
}

public interface Interface2 {
    default void method() {
        System.out.println("Interface2 default method");
    }
}

public class MyClass implements Interface1, Interface2 {
    // Must override to resolve conflict
    @Override
    public void method() {
        // Can choose which interface method to call
        Interface1.super.method(); // Call Interface1's version
        // Or Interface2.super.method(); // Call Interface2's version
        // Or provide completely new implementation
        System.out.println("MyClass custom implementation");
    }
}

Static Methods in Interfaces

Static methods belong to the interface itself (Java 8+):

public interface MathUtils {
    // Static constants
    double PI = 3.14159;
    double E = 2.71828;
    
    // Abstract method
    double calculate();
    
    // Static utility methods
    static double circleArea(double radius) {
        return PI * radius * radius;
    }
    
    static double circleCircumference(double radius) {
        return 2 * PI * radius;
    }
    
    static boolean isPositive(double value) {
        return value > 0;
    }
    
    // Static method can call other static methods
    static void printCircleInfo(double radius) {
        if (isPositive(radius)) {
            System.out.println("Circle with radius " + radius);
            System.out.println("Area: " + circleArea(radius));
            System.out.println("Circumference: " + circleCircumference(radius));
        } else {
            System.out.println("Invalid radius: " + radius);
        }
    }
}

// Usage - call static methods on interface
MathUtils.printCircleInfo(5.0);
double area = MathUtils.circleArea(3.0);

Functional Interfaces

Interfaces with exactly one abstract method:

Basic Functional Interface

@FunctionalInterface
public interface Calculator {
    double calculate(double a, double b);
    
    // Can have default and static methods
    default void printResult(double a, double b) {
        System.out.println("Result: " + calculate(a, b));
    }
    
    static Calculator getAdder() {
        return (a, b) -> a + b;
    }
}

// Usage with lambda expressions
public class CalculatorDemo {
    public static void main(String[] args) {
        // Lambda expressions
        Calculator adder = (a, b) -> a + b;
        Calculator multiplier = (a, b) -> a * b;
        Calculator divider = (a, b) -> b != 0 ? a / b : 0;
        
        // Method references
        Calculator maxCalculator = Math::max;
        Calculator minCalculator = Math::min;
        
        // Usage
        System.out.println("Addition: " + adder.calculate(10, 5));
        System.out.println("Multiplication: " + multiplier.calculate(10, 5));
        System.out.println("Division: " + divider.calculate(10, 5));
        System.out.println("Max: " + maxCalculator.calculate(10, 5));
        System.out.println("Min: " + minCalculator.calculate(10, 5));
    }
}

Built-in Functional Interfaces

import java.util.function.*;
import java.util.List;
import java.util.Arrays;

public class FunctionalInterfaceExamples {
    public static void main(String[] args) {
        // Predicate<T> - test a condition
        Predicate<String> isEmpty = String::isEmpty;
        Predicate<Integer> isEven = n -> n % 2 == 0;
        
        System.out.println("Is empty: " + isEmpty.test(""));
        System.out.println("Is even: " + isEven.test(4));
        
        // Function<T, R> - transform input to output
        Function<String, Integer> stringLength = String::length;
        Function<Integer, String> intToString = Object::toString;
        
        System.out.println("Length: " + stringLength.apply("Hello"));
        System.out.println("String: " + intToString.apply(42));
        
        // Consumer<T> - perform action on input
        Consumer<String> printer = System.out::println;
        Consumer<List<String>> listPrinter = list -> list.forEach(System.out::println);
        
        printer.accept("Hello World");
        listPrinter.accept(Arrays.asList("A", "B", "C"));
        
        // Supplier<T> - provide a value
        Supplier<Double> randomValue = Math::random;
        Supplier<String> greeting = () -> "Hello";
        
        System.out.println("Random: " + randomValue.get());
        System.out.println("Greeting: " + greeting.get());
        
        // BiFunction<T, U, R> - two inputs, one output
        BiFunction<String, String, String> concatenator = (a, b) -> a + " " + b;
        System.out.println("Concatenated: " + concatenator.apply("Hello", "World"));
    }
}

Interface Inheritance

Interfaces can extend other interfaces:

Single Interface Inheritance

public interface Animal {
    void eat();
    void sleep();
    
    default void breathe() {
        System.out.println("Animal is breathing");
    }
}

public interface Mammal extends Animal {
    void giveBirth();
    void produceMilk();
    
    // Can override default methods
    @Override
    default void breathe() {
        System.out.println("Mammal is breathing with lungs");
    }
    
    // Add new default method
    default void regulateTemperature() {
        System.out.println("Mammal is regulating body temperature");
    }
}

public class Dog implements Mammal {
    @Override
    public void eat() {
        System.out.println("Dog is eating dog food");
    }
    
    @Override
    public void sleep() {
        System.out.println("Dog is sleeping in its bed");
    }
    
    @Override
    public void giveBirth() {
        System.out.println("Dog is giving birth to puppies");
    }
    
    @Override
    public void produceMilk() {
        System.out.println("Dog is producing milk for puppies");
    }
}

Multiple Interface Inheritance

public interface Flyable {
    void fly();
    void land();
    
    default void takeOff() {
        System.out.println("Taking off...");
        fly();
    }
}

public interface Swimmable {
    void swim();
    void dive();
    
    default void enterWater() {
        System.out.println("Entering water...");
        swim();
    }
}

// Interface inheriting from multiple interfaces
public interface Amphibious extends Flyable, Swimmable {
    void transition(); // Switch between flying and swimming
    
    default void performAerialManeuver() {
        takeOff(); // From Flyable
        fly();     // From Flyable
        land();    // From Flyable
    }
    
    default void performAquaticManeuver() {
        enterWater(); // From Swimmable
        dive();       // From Swimmable
        swim();       // From Swimmable
    }
}

public class Duck implements Amphibious {
    @Override
    public void fly() {
        System.out.println("Duck is flying");
    }
    
    @Override
    public void land() {
        System.out.println("Duck is landing");
    }
    
    @Override
    public void swim() {
        System.out.println("Duck is swimming");
    }
    
    @Override
    public void dive() {
        System.out.println("Duck is diving underwater");
    }
    
    @Override
    public void transition() {
        System.out.println("Duck is transitioning between air and water");
    }
}

Real-World Examples

1. Event Handling System

public interface EventListener<T> {
    void onEvent(T event);
    
    default void onError(Exception error) {
        System.err.println("Error handling event: " + error.getMessage());
    }
}

public interface Lifecycle {
    void start();
    void stop();
    
    default boolean isRunning() {
        return false; // Default implementation
    }
}

public class UserEvent {
    private String userId;
    private String action;
    private long timestamp;
    
    public UserEvent(String userId, String action) {
        this.userId = userId;
        this.action = action;
        this.timestamp = System.currentTimeMillis();
    }
    
    // Getters
    public String getUserId() { return userId; }
    public String getAction() { return action; }
    public long getTimestamp() { return timestamp; }
}

public class EventProcessor implements EventListener<UserEvent>, Lifecycle {
    private boolean running = false;
    private List<UserEvent> processedEvents = new ArrayList<>();
    
    @Override
    public void start() {
        running = true;
        System.out.println("Event processor started");
    }
    
    @Override
    public void stop() {
        running = false;
        System.out.println("Event processor stopped");
    }
    
    @Override
    public boolean isRunning() {
        return running;
    }
    
    @Override
    public void onEvent(UserEvent event) {
        if (!running) {
            System.out.println("Processor not running, ignoring event");
            return;
        }
        
        System.out.println("Processing event: " + event.getAction() + 
                          " for user " + event.getUserId());
        processedEvents.add(event);
    }
    
    public List<UserEvent> getProcessedEvents() {
        return new ArrayList<>(processedEvents);
    }
}

2. Repository Pattern with Interfaces

public interface Repository<T, ID> {
    void save(T entity);
    T findById(ID id);
    List<T> findAll();
    void delete(T entity);
    boolean exists(ID id);
    
    default void saveAll(List<T> entities) {
        entities.forEach(this::save);
    }
    
    default long count() {
        return findAll().size();
    }
}

public interface UserRepository extends Repository<User, Long> {
    List<User> findByEmail(String email);
    List<User> findByAge(int age);
    List<User> findActiveUsers();
    
    default List<User> findAdults() {
        return findAll().stream()
                       .filter(user -> user.getAge() >= 18)
                       .collect(Collectors.toList());
    }
}

public class User {
    private Long id;
    private String name;
    private String email;
    private int age;
    private boolean active;
    
    // Constructors, getters, setters
    public User(Long id, String name, String email, int age) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.age = age;
        this.active = true;
    }
    
    // Getters and setters
    public Long getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }
    public int getAge() { return age; }
    public boolean isActive() { return active; }
    public void setActive(boolean active) { this.active = active; }
}

public class InMemoryUserRepository implements UserRepository {
    private Map<Long, User> users = new HashMap<>();
    private AtomicLong idCounter = new AtomicLong(1);
    
    @Override
    public void save(User user) {
        if (user.getId() == null) {
            user = new User(idCounter.getAndIncrement(), user.getName(), 
                           user.getEmail(), user.getAge());
        }
        users.put(user.getId(), user);
    }
    
    @Override
    public User findById(Long id) {
        return users.get(id);
    }
    
    @Override
    public List<User> findAll() {
        return new ArrayList<>(users.values());
    }
    
    @Override
    public void delete(User user) {
        users.remove(user.getId());
    }
    
    @Override
    public boolean exists(Long id) {
        return users.containsKey(id);
    }
    
    @Override
    public List<User> findByEmail(String email) {
        return users.values().stream()
                   .filter(user -> user.getEmail().equals(email))
                   .collect(Collectors.toList());
    }
    
    @Override
    public List<User> findByAge(int age) {
        return users.values().stream()
                   .filter(user -> user.getAge() == age)
                   .collect(Collectors.toList());
    }
    
    @Override
    public List<User> findActiveUsers() {
        return users.values().stream()
                   .filter(User::isActive)
                   .collect(Collectors.toList());
    }
}

Best Practices

1. Interface Segregation Principle

// Bad - Large interface
interface BadPrinter {
    void print();
    void scan();
    void fax();
    void email();
}

// Good - Segregated interfaces
interface Printer {
    void print();
}

interface Scanner {
    void scan();
}

interface FaxMachine {
    void fax();
}

interface EmailSender {
    void email();
}

// Classes implement only what they need
class SimplePrinter implements Printer {
    @Override
    public void print() {
        System.out.println("Printing document");
    }
}

class AllInOnePrinter implements Printer, Scanner, FaxMachine, EmailSender {
    @Override
    public void print() { System.out.println("Printing"); }
    
    @Override
    public void scan() { System.out.println("Scanning"); }
    
    @Override
    public void fax() { System.out.println("Faxing"); }
    
    @Override
    public void email() { System.out.println("Emailing"); }
}

2. Use Composition with Interfaces

public interface Logger {
    void log(String message);
}

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

public class NotificationService {
    private Logger logger;
    private EmailService emailService;
    
    public NotificationService(Logger logger, EmailService emailService) {
        this.logger = logger;
        this.emailService = emailService;
    }
    
    public void sendNotification(String recipient, String message) {
        logger.log("Sending notification to: " + recipient);
        emailService.sendEmail(recipient, "Notification", message);
        logger.log("Notification sent successfully");
    }
}

3. Provide Meaningful Default Methods

public interface Cache<K, V> {
    void put(K key, V value);
    V get(K key);
    void remove(K key);
    void clear();
    
    // Meaningful default methods
    default boolean containsKey(K key) {
        return get(key) != null;
    }
    
    default V getOrDefault(K key, V defaultValue) {
        V value = get(key);
        return value != null ? value : defaultValue;
    }
    
    default void putIfAbsent(K key, V value) {
        if (!containsKey(key)) {
            put(key, value);
        }
    }
}

Summary

Java interfaces provide:

  • Contract Definition: Specify what classes must implement
  • Multiple Inheritance: Implement multiple interfaces
  • Abstraction: Define behavior without implementation
  • Default Methods: Provide default implementations (Java 8+)
  • Static Methods: Utility methods in interfaces (Java 8+)
  • Functional Programming: Support for lambda expressions

Key benefits:

  • Loose Coupling: Depend on abstractions, not concrete classes
  • Flexibility: Easy to swap implementations
  • Testability: Mock interfaces for unit testing
  • Extensibility: Add new functionality without breaking existing code

Use interfaces to define contracts, achieve multiple inheritance of type, and create flexible, maintainable, and testable code architectures.