Introduction
If Lambda Expression are the stars of Java 8, Functional Interfaces are certainly the stage where they can shine. Without understanding Functional Interfaces, Lambda Expressions may seem like syntactic magic. In reality, they are simply a more concise way to implement them.
In this post, we will explore what functional interfaces are, why they exist, how they work, and how they serve as the foundation of modern Java programming.
What is Functional Interface?
A functional interface is an interface that has exactly one abstract method. This single method is often called the SAM (Single Abstract Method).
Because they have only one abstract method, functional interfaces can be implicitly converted to lambda expressions. This is the magic that makes Java 8’s functional programming possible.
The @FunctionalInterface annotation
Although not mandatory, the @FunctionalInterface annotation is highly recommended. This annotation helps prevent accidental violations of the functional interface rule, if you try to add another abstract method the Java compiler will generate an error.
// A functional interface with one abstract method
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}
public class FunctionalInterfaceSample {
public static void main(String[] args) {
Calculator addition = (a, b) -> a + b;
System.out.println(addition.calculate(10, 20));
Calculator subtraction = (a, b) -> a - b;
System.out.println(subtraction.calculate(10, 20));
Calculator multiplication = (a, b) -> a * b;
System.out.println(multiplication.calculate(10, 20));
Calculator division = (a, b) -> a / b;
System.out.println(division.calculate(10, 20));
}
}
Code language: Java (java)
In this example, the Calculator interface defines a single abstract method, making it a functional interface. Inside the main method, different lambda expressions are used to implement addition, subtraction, multiplication, and division. Each lambda provides a specific behavior for the calculate method without creating separate classes. This shows how functional interfaces allow us to write concise and flexible code in Java 8.
Default and Static Methods: The Evolution of Interfaces
Before Java 8, interfaces were very limited: they could only declare abstract methods and constants. They are no longer just empty contracts. But, this created a big problem — if you added a new method to an interface, all classes implementing it would break.
Java 8 solved this by introducing default and static methods, allowing interfaces to contain behavior.
- Default Methods: Use the default keyword. they provide a default implementation that classes can use or override.
- Static Methods: Belong to the interface class itself and can be called without an object instance.
These do not count toward the “Single Abstract Method” (SAM) rule because they are already implemented!
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
// Simple DTO (Data Transfer Object)
class UserDTO {
String name;
UserDTO(String name) { this.name = name; }
}
// Simple Entity (Database Model)
class UserEntity {
String name;
UserEntity(String name) { this.name = name; }
@Override
public String toString() { return "UserEntity{name='" + name + "'}"; }
}
@FunctionalInterface
interface EntityConverter<E, D> {
// 1. The Single Abstract Method (SAM)
E convert(D dto);
// 2. Default Method
default List<E> convertList(List<D> dtos) {
if (dtos == null || dtos.isEmpty()) return Collections.emptyList();
return dtos.stream()
.map(this::convert) // Calls the lambda implementation
.collect(Collectors.toList());
}
// 3. Static Method
static <D> boolean isPresent(D dto) {
return dto != null;
}
}
public class InterfaceUse {
public static void main(String[] args) {
// --- 1. Using the Static Method ---
UserDTO myDto = new UserDTO("W5HH");
if (EntityConverter.isPresent(myDto)) {
System.out.println("The DTO is present!");
}
// --- 2. Implementing the SAM with a Lambda ---
// Here, we define the 'convert' logic: take a DTO and return a new Entity
EntityConverter<UserEntity, UserDTO> userConverter = dto -> new UserEntity(dto.name);
// --- 3. Using the Default Method ---
List<UserDTO> dtoList = Arrays.asList(new UserDTO("Java"), new UserDTO("8"));
// We didn't implement 'convertList', but we can use it!
List<UserEntity> entityList = userConverter.convertList(dtoList);
System.out.println("Converted List: " + entityList);
}
}Code language: Java (java)
Did functional interfaces exist before Java 8?
Yes, they did. It might seem strange, but the Java API contained many examples of functional interfaces even before Java 8. While the @FunctionalInterface annotation and Lambda expressions were introduced in Java 8, the concept of an interface with a single abstract method (SAM) has been around since Java 1.0.
Before Java 8, we didn’t call them “functional interfaces”; we just thought of them as interfaces that we typically implemented using Anonymous Inner Classes.
Classic Examples from Pre-Java 8:
java.lang.Runnable: Used for threads (since 1.0).java.awt.event.ActionListener: Used for GUI events (since 1.1).java.util.Comparator: Used for sorting (since 1.2).
For better understanding, below are examples of using the Runnable interface before and after Java 8:
// 1. Implementing the Runnable interface in a separate named class
class RunnableImpl implements Runnable {
@Override
public void run() {
System.out.println("Running via a concrete Runnable implementation class");
}
}
// 5. Extending the Thread class directly (Less flexible than Runnable)
class ThreadExtension extends Thread {
@Override
public void run() {
System.out.println("Running via a class that extends Thread");
}
}
public class FunctionalThread {
public static void main(String[] args) {
// 1. Instantiating a specific implementation of Runnable
Runnable r1 = new RunnableImpl();
Thread t1 = new Thread(r1);
t1.start();
// 2. Using an Anonymous Inner Class assigned to a reference variable
Runnable r2 = new Runnable() {
@Override
public void run() {
System.out.println("Running via an anonymous Runnable implementation");
}
};
Thread t2 = new Thread(r2);
t2.start();
// 3. Passing an Anonymous Inner Class directly as a constructor argument
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Running via an inline anonymous inner class");
}
});
t3.start();
// 4. Using a Lambda Expression (The modern, functional way for Runnable)
Thread t4 = new Thread(() -> System.out.println("Running via a Lambda Expression"));
t4.start();
// 5. Instantiating the subclass of Thread
Thread t5 = new ThreadExtension();
t5.start();
}
}
Code language: Java (java)
Conclusion
Functional Interfaces are the silent heroes of Java 8. By defining a clear contract through the Single Abstract Method (SAM) principle, they allowed Java to embrace functional programming without losing its object-oriented roots.
As we’ve seen, whether you are using classic interfaces like Runnable or creating your own like Calculator, the real power lies in the ability to pass behavior as data. This not only reduces boilerplate code but also makes our applications more readable and easier to maintain.
In the next post, we will take this a step further. Now that you know how Functional Interfaces provide the “stage,” we will look at Method References—the ultimate shortcut to make your functional code even cleaner.
Stay tuned!
