Functional Interfaces
Before Java 8, passing behavior as a parameter meant creating verbose Anonymous Inner Classes. This led to the “vertical problem” – writing 6 lines of code to express a single, simple concept.
To solve this, Java introduced Lambda Expressions, allowing us to treat functions as first-class citizens. But because Java is fundamentally Object-Oriented, it needed a way to bridge the gap between functional code and its strict type system. The solution? Functional Interfaces.
Every “Lambda” expression you write in Java is actually an implementation of a Functional Interface.
1. What is a Functional Interface?
It is an interface that contains exactly one abstract method. You can mark it with @FunctionalInterface to have the compiler enforce this rule. This is called a Single Abstract Method (SAM) interface.
@FunctionalInterface
public interface Greeter {
void sayHello(String name);
}
The “Genesis”: From Inner Class to Lambda
Let’s see how a Functional Interface replaces an Anonymous Inner Class.
2. Standard Functional Interfaces
You rarely need to create your own functional interfaces for common tasks. Java provides a robust set in the java.util.function package. The four pillars you must memorize are:
| Interface | Abstract Method | Purpose | Example |
|---|---|---|---|
| Predicate<T> | boolean test(T t) |
Evaluates a condition and returns true/false. | s -> s.length() > 5 |
| Consumer<T> | void accept(T t) |
Takes an argument, does something, returns nothing. | s -> System.out.println(s) |
| Function<T, R> | R apply(T t) |
Transforms an input of type T into an output of type R. |
s -> s.length() |
| Supplier<T> | T get() |
Takes no arguments, returns a result (often used for lazy generation). | () -> Math.random() |
The Primitives Overhead (Advanced)
Generics in Java (like Consumer<T>) only work with Objects. If you use Consumer<Integer>, passing a primitive int forces Java to wrap it into an Integer object (Autoboxing), which degrades performance in high-throughput applications.
To prevent this, Java provides primitive-specialized interfaces:
IntPredicate,LongPredicate,DoublePredicateIntConsumer,LongConsumer,DoubleConsumerToIntFunction<T>,LongToIntFunction, etc.
Always prefer primitive specializations when processing large streams of numbers.
3. Method References
Sometimes, your Lambda expression does nothing but call an existing method. In those cases, you can use the :: syntax for even cleaner, more readable code. It lets you pass the method itself as an argument.
// LAMBDA: We explicitly receive 'u' and pass it to println
users.forEach(u -> System.out.println(u));
// METHOD REFERENCE: "Just pass the argument directly to this method"
users.forEach(System.out::println);
The 4 Types of Method References
| Type | Syntax | Lambda Equivalent | Example |
|---|---|---|---|
| Static Method | Class::staticMethod |
x -> Class.staticMethod(x) |
Math::max |
| Instance Method (Specific Object) | instance::method |
x -> instance.method(x) |
System.out::println |
| Instance Method (Arbitrary Object) | Class::method |
(obj, x) -> obj.method(x) |
String::toLowerCase |
| Constructor | Class::new |
() -> new Class() |
ArrayList::new |
4. Interactive: Lambda Lab
Test your intuition. Which Standard Functional Interface is the best fit for the requirement?
5. Default & Static Methods in Functional Interfaces
You might wonder: If a Functional Interface can only have one abstract method, how does Predicate have .and(), .or(), and .negate() methods?
The rule is strictly one abstract method. However, an interface can have as many default or static methods as it wants, because those methods already have implementations!
Predicate<String> startsWithA = s -> s.startsWith("A");
Predicate<String> endsWithZ = s -> s.endsWith("Z");
// We can chain predicates using the default .and() method!
Predicate<String> aToZ = startsWithA.and(endsWithZ);
boolean result = aToZ.test("Alcatraz"); // true