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.