L14: Interfaces Continued

Interfaces Continued

Interfaces in Java define contracts that implementing classes must fulfill. They are the primary mechanism for achieving abstraction and enabling multiple inheritance in Java.

public interface SimpleInterface {
    void method1();  // abstract by default
    int method2(String param);  // must be implemented
}
ℹ️
All interface methods are implicitly public and abstract unless explicitly marked as default or static. You cannot declare interface methods as private or protected.

Default Methods

Default methods were introduced in Java 8 to allow adding new methods to interfaces without breaking existing implementations.

public interface Vehicle {
    void start();
    void stop();
    
    default void horn() {
        System.out.println("Beep!");
    }
}
⚠️
Use default methods sparingly. While they provide backward compatibility, overuse can lead to the same problems as multiple inheritance in other languages.

Multiple Inheritance Resolution

When dealing with multiple interfaces that have conflicting default methods, Java follows specific resolution rules:

I’ll explain multiple inheritance resolution in Java interfaces in detail.

Let’s break down multiple inheritance resolution with concrete examples:

1. Basic Conflict Scenario

// First interface
interface Swimmer {
    default void move() {
        System.out.println("Swimming");
    }
}

// Second interface
interface Runner {
    default void move() {
        System.out.println("Running");
    }
}
ℹ️
When a class implements both interfaces, we have a “diamond problem” - Java needs to know which move() method to use.

2. Resolution Rules in Order of Priority:

a) Rule 1: Class implementation wins
class Athlete implements Runner, Swimmer {
    // This implementation will ALWAYS be used
    @Override
    public void move() {
        System.out.println("Moving like an athlete");
    }
}
b) Rule 2: Most specific interface wins
interface SportsPerson extends Runner {
    // This is more specific than Runner
    default void move() {
        System.out.println("Moving like a sports person");
    }
}

// SportsPerson's move() would be used because it's more specific than Runner
class Amateur implements Runner, SportsPerson {
    // No override needed - SportsPerson's version is used
}
c) Rule 3: Explicit disambiguation required
class Triathlete implements Runner, Swimmer {
    // Must explicitly choose or provide new implementation
    @Override
    public void move() {
        // Option 1: Call specific interface's implementation
        Runner.super.move();  // Uses Runner's version
        
        // Option 2: Call other interface's implementation
        // Swimmer.super.move();
        
        // Option 3: Do something completely different
        // System.out.println("Moving like a triathlete");
    }
}

3. Practical Example with Multiple Methods:

interface Device {
    default void turnOn() {
        System.out.println("Device turning on");
    }
    
    default void turnOff() {
        System.out.println("Device turning off");
    }
}

interface Vehicle {
    default void turnOn() {
        System.out.println("Vehicle starting engine");
    }
    
    default void turnOff() {
        System.out.println("Vehicle stopping engine");
    }
}

class ElectricCar implements Device, Vehicle {
    // Must resolve conflict for turnOn and turnOff
    
    @Override
    public void turnOn() {
        // Can combine both behaviors
        Device.super.turnOn();
        Vehicle.super.turnOn();
        System.out.println("Electric car ready");
    }
    
    @Override
    public void turnOff() {
        System.out.println("Electric car shutting down");
        Vehicle.super.turnOff();
        Device.super.turnOff();
    }
}
⚠️

Common Pitfalls to Avoid:

  1. Don’t assume Java will automatically choose between conflicting methods
  2. Always explicitly override when there’s a conflict
  3. Be careful when combining multiple interface behaviors

4. Testing the Resolution:

public class Main {
    public static void main(String[] args) {
        ElectricCar tesla = new ElectricCar();
        tesla.turnOn();
        // Output:
        // Device turning on
        // Vehicle starting engine
        // Electric car ready
        
        tesla.turnOff();
        // Output:
        // Electric car shutting down
        // Vehicle stopping engine
        // Device turning off
    }
}

5. Advanced Scenario - Multiple Levels:

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

interface B extends A {
    default void show() { System.out.println("B"); }
}

interface C extends A {
    default void show() { System.out.println("C"); }
}

class D implements B, C {
    // Must override because B and C both provide show()
    @Override
    public void show() {
        B.super.show(); // Choosing B's implementation
    }
}
💡

Best Practices:

  1. Always make the resolution choice explicit in your code
  2. Document why you chose a particular implementation
  3. Consider whether you need both interfaces
  4. Think about creating a new interface that extends both if the combination is common

Static Methods in Interfaces

Static methods provide utility functions that belong to the interface itself. They cannot be overridden by implementing classes.

public interface MathOperations {
    static double square(double num) {
        return num * num;
    }
    
    static boolean isPrime(int num) {
        if (num <= 1) return false;
        for (int i = 2; i <= Math.sqrt(num); i++) {
            if (num % i == 0) return false;
        }
        return true;
    }
}

Interface Constants

While interfaces can contain constants, this feature should be used carefully:

public interface DatabaseConfig {
    int MAX_CONNECTIONS = 100;  // implicitly public static final
    String DEFAULT_URL = "jdbc:mysql://localhost:3306/";
}
⚠️
Creating interfaces solely for constants is an anti-pattern. Consider using enums or configuration classes instead.

Functional Interfaces

Functional interfaces, marked with @FunctionalInterface, enable lambda expressions and are crucial for functional programming in Java.

@FunctionalInterface
public interface Processor<T> {
    T process(T input);
    
    default Processor<T> andThen(Processor<T> after) {
        return input -> after.process(this.process(input));
    }
}

Common Implementation Patterns

Repository Pattern

public interface Repository<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
    void save(T entity);
    void delete(ID id);
}
💡

Design Tips:

  • Keep interfaces focused on a single responsibility
  • Use generics for type safety
  • Consider providing default methods for common operations
  • Document expected behavior clearly

Testing Strategies

Testing interfaces typically involves both testing implementations and using mocks:

@Test
void testUserRepository() {
    // Mock creation
    UserRepository repository = mock(UserRepository.class);
    
    // Define behavior
    when(repository.findById(1L)).thenReturn(Optional.of(new User("John")));
    
    // Test
    Optional<User> user = repository.findById(1L);
    assertTrue(user.isPresent());
    assertEquals("John", user.get().getName());
}

Common Pitfalls

Interface Pollution

Avoid creating large, monolithic interfaces:

// Bad example
public interface SuperWorker {
    void work();
    void eat();
    void sleep();
    void calculateSalary();
    void attendMeeting();
}

// Better approach
public interface Worker {
    void work();
}

public interface PayrollCalculator {
    void calculateSalary();
}
⚠️

Signs of Poor Interface Design:

  1. Too many unrelated methods
  2. Methods that share implementation details
  3. Methods that would naturally share state
  4. Interfaces that are frequently implemented together

Remember: The power of interfaces lies in their ability to define clear contracts while maintaining flexibility in implementation. Focus on creating interfaces that are cohesive, focused, and easy to implement.