Understanding Interfaces in Java
Interfaces are one of the fundamental pillars of object-oriented programming in Java. They allow you to define contracts for classes, ensuring that certain operations will be implemented without dictating how these operations should be performed. In this post, we will explore what interfaces are, their purpose, basic and complex usage examples, and why using interfaces is a better practice than using inheritance. We will also look at a real-world use case example.
What is an Interface?
In Java, an interface is a reference type, similar to a class, but it can only contain constants, method signatures, default methods, static methods, and nested types. Methods in an interface are implicitly public
and abstract
, unless they are default
or static
.
Default Methods
Default methods were introduced in Java 8. A default method in an interface is a method that has a default implementation. This allows you to add new methods to existing interfaces without breaking the code of classes that already implement those interfaces.
Example 1: Default Method in an Interface
public interface Vehicle {
void start();
void stop();
default void honk() {
System.out.println("Honking");
}
}
public class Car implements Vehicle {
@Override
public void start() {
System.out.println("Car starting");
}
@Override
public void stop() {
System.out.println("Car stopping");
}
}
public class Main {
public static void main(String[] args) {
Vehicle myCar = new Car();
myCar.start();
myCar.honk(); // Default method called
myCar.stop();
}
}
Example 2: Default Method with Common Implementation
public interface Logger {
void log(String message);
default void logError(String message) {
log("ERROR: " + message);
}
}
public class ConsoleLogger implements Logger {
@Override
public void log(String message) {
System.out.println(message);
}
}
public class Main {
public static void main(String[] args) {
Logger logger = new ConsoleLogger();
logger.log("A simple message");
logger.logError("A simple error message"); // Default method called
}
}
Why Use Default Methods?
1. Maintain Backward Compatibility:
Default methods allow you to add new methods to an interface without requiring that all classes that implement the interface provide an implementation for the new method. This is crucial for maintaining backward compatibility.
2. Share Common Code:
They allow you to provide a default implementation that can be shared by all classes that implement the interface, reducing code duplication.
Practical Examples of Using Default Methods
Example 1: Adding Functionality to an Existing Library
Imagine you have an interface ListProcessor
used throughout your application to process lists.
public interface ListProcessor {
void processList(List<String> list);
default void processAndPrintList(List<String> list) {
processList(list);
list.forEach(System.out::println);
}
}
public class UpperCaseListProcessor implements ListProcessor {
@Override
public void processList(List<String> list) {
list.replaceAll(String::toUpperCase);
}
}
Here, the processAndPrintList
method was added without breaking the existing implementations of ListProcessor
.
Example 2: Implementing Utility Methods
Consider an interface TimeSeries
for handling time series data.
public interface TimeSeries {
double getValue(long timestamp);
default double getAverage(long start, long end) {
double sum = 0;
int count = 0;
for (long ts = start; ts <= end; ts++) {
sum += getValue(ts);
count++;
}
return count == 0 ? 0 : sum / count;
}
}
public class SimpleTimeSeries implements TimeSeries {
private Map<Long, Double> data;
public SimpleTimeSeries(Map<Long, Double> data) {
this.data = data;
}
@Override
public double getValue(long timestamp) {
return data.getOrDefault(timestamp, 0.0);
}
}
The getAverage
method provides a convenient way to calculate the average of values over a time interval without needing to be reimplemented in each concrete class.
Basic Examples
Let's start with some simple examples of interfaces to understand their basic functionality.
Example 1: Simple Interface
public interface Drawable {
void draw();
}
public class Circle implements Drawable {
@Override
public void draw() {
System.out.println("Drawing a Circle");
}
}
public class Square implements Drawable {
@Override
public void draw() {
System.out.println("Drawing a Square");
}
}
Example 2: Interface with Default Methods
public interface Vehicle {
void start();
void stop();
default void honk() {
System.out.println("Honking");
}
}
public class Car implements Vehicle {
@Override
public void start() {
System.out.println("Car starting");
}
@Override
public void stop() {
System.out.println("Car stopping");
}
}
Example 3: Interface with Static Methods
public interface MathOperations {
int add(int a, int b);
static int multiply(int a, int b) {
return a * b;
}
}
public class SimpleMath implements MathOperations {
@Override
public int add(int a, int b) {
return a + b;
}
}
Complex Examples
Now let's look at some more complex examples of interfaces, showing how they can be used in more advanced scenarios.
Example 1: Interface for Module Communication
public interface MessageService {
void sendMessage(String message, String receiver);
}
public class EmailService implements MessageService {
@Override
public void sendMessage(String message, String receiver) {
// logic to send email
System.out.println("Email sent to " + receiver + " with Message=" + message);
}
}
public class SMSService implements MessageService {
@Override
public void sendMessage(String message, String receiver) {
// logic to send SMS
System.out.println("SMS sent to " + receiver + " with Message=" + message);
}
}
Example 2: Interface for Sorting Strategies
public interface SortStrategy {
void sort(int[] array);
}
public class BubbleSort implements SortStrategy {
@Override
public void sort(int[] array) {
// Implementation of Bubble Sort
}
}
public class QuickSort implements SortStrategy {
@Override
public void sort(int[] array) {
// Implementation of Quick Sort
}
}
Example 3: Interface for Plugins
public interface Plugin {
void initialize();
void execute();
}
public class PluginA implements Plugin {
@Override
public void initialize() {
// Initialization of PluginA
}
@Override
public void execute() {
// Execution of PluginA
}
}
public class PluginB implements Plugin {
@Override
public void initialize() {
// Initialization of PluginB
}
@Override
public void execute() {
// Execution of PluginB
}
}
Example 4: Interface for Middleware
public interface Middleware {
void process(Request request, Response response, Middleware next);
}
public class AuthenticationMiddleware implements Middleware {
@Override
public void process(Request request, Response response, Middleware next) {
// Authentication check
if (authenticated(request)) {
next.process(request, response, next);
} else {
response.setStatus(401);
}
}
}
public class LoggingMiddleware implements Middleware {
@Override
public void process(Request request, Response response, Middleware next) {
// Request logging
System.out.println("Request: " + request);
next.process(request, response, next);
}
}
Why Using Interfaces is Better Than Inheritance
Flexibility and Reusability
Interfaces provide more flexibility than inheritance because a class can implement multiple interfaces but can inherit from only one class. This allows you to create more reusable and modular components.
Decoupling
Interfaces help decouple the code, allowing different parts of a system to evolve independently. The implementation of an interface can change without affecting the other parts of the system that depend on that interface.
Following SOLID Principles
Single Responsibility Principle: Each interface should have a single responsibility.
Open/Closed Principle: Classes should be open for extension but closed for modification.
Liskov Substitution Principle: Objects of a superclass should be replaceable with objects of a subclass without altering the expected behavior.
Interface Segregation Principle: Many specific interfaces are better than one general interface.
Dependency Inversion Principle: Depend on abstractions (interfaces), not on concrete implementations.
Real-World Use Case Example
Consider an online payment system that supports multiple payment methods. Using interfaces can facilitate adding new payment methods without modifying existing code.
public interface PaymentMethod {
void pay(double amount);
}
public class CreditCardPayment implements PaymentMethod {
@Override
public void pay(double amount) {
// logic to pay with credit card
System.out.println("Paid " + amount + " using Credit Card");
}
}
public class PayPalPayment implements PaymentMethod {
@Override
public void pay(double amount) {
// logic to pay with PayPal
System.out.println("Paid " + amount + " using PayPal");
}
}
public class PaymentProcessor {
private PaymentMethod paymentMethod;
public PaymentProcessor(PaymentMethod paymentMethod) {
this.paymentMethod = paymentMethod;
}
public void processPayment(double amount) {
paymentMethod.pay(amount);
}
}
// Usage
public class Main {
public static void main(String[] args) {
PaymentProcessor creditCardPayment = new PaymentProcessor(new CreditCardPayment());
creditCardPayment.processPayment(100.0);
PaymentProcessor payPalPayment = new PaymentProcessor(new PayPalPayment());
payPalPayment.processPayment(200.0);
}
}
Conclusion
Interfaces are a powerful tool in Java that allows greater flexibility, reusability, and maintenance of code. They are preferable to inheritance in many cases due to their ability to promote decoupling and adhere to SOLID principles. Using interfaces enables you to build robust and extensible systems that can evolve with the needs of your project.
Subscribe to my newsletter
Read articles from André Felipe Costa Bento directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
André Felipe Costa Bento
André Felipe Costa Bento
Fullstack Software Engineer.