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.

```java // 1. We must instantiate an anonymous class implementing the interface Greeter g = new Greeter() { @Override public void sayHello(String name) { System.out.println("Hello, " + name); } }; g.sayHello("Alice"); ```
```java // 1. The interface and method name are inferred! Greeter g = (name) -> System.out.println("Hello, " + name); g.sayHello("Alice"); ```

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, DoublePredicate
  • IntConsumer, LongConsumer, DoubleConsumer
  • ToIntFunction<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?

Question 1/4: "Print a user's name to the console."
Choose the best interface.

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