Functional Programming

Before Java 8, Java was almost exclusively imperative and object-oriented. You defined how to do things, step-by-step, using objects and mutable state. With Java 8, the language introduced functional programming concepts, allowing you to write more concise, declarative code.

In this chapter, we’ll explore the building blocks of functional Java: Lambda Expressions, Functional Interfaces, and Method References.

1. Imperative vs. Declarative

  • Imperative: Focuses on how to execute, defining control flow explicitly (loops, if-statements).
  • Declarative: Focuses on what to achieve, abstracting away the control flow (filters, maps).

Imperative (Old)

// Imperative: How to filter a list
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> filtered = new ArrayList<>();
for (String name : names) {
    if (name.startsWith("A")) {
        filtered.add(name);
    }
}

Declarative (New)

// Declarative: What we want (Java 8+)
List<String> filtered = names.stream()
    .filter(name -> name.startsWith("A"))
    .toList(); // Java 16+

2. Lambda Expressions

A Lambda Expression is essentially an anonymous function—a block of code that can be passed around to execute. It provides a clear and concise way to represent a method interface using an expression.

Anatomy of a Lambda

(a, b)
Parameters
Token
{ return a + b; }
Body

Comparison: Java vs. Go

Java uses Single Abstract Method (SAM) interfaces to type-check lambdas. Go has first-class functions.

Java (Functional Interface)

// Java requires an interface
@FunctionalInterface
interface MathOp {
    int operate(int a, int b);
}

public class Main {
    public static void main(String[] args) {
        // Lambda implements the interface
        MathOp add = (a, b) -> a + b;
        System.out.println(add.operate(5, 3));
    }
}

Go (First-Class Functions)

package main

import "fmt"

func main() {
    // Go functions are types
    type MathOp func(int, int) int

    // Function literal assigned to variable
    add := func(a, b int) int {
        return a + b
    }

    fmt.Println(add(5, 3))
}

3. Functional Interfaces

A Functional Interface is an interface that contains exactly one abstract method. It can have multiple default or static methods, but only one abstract one. They are the target types for lambda expressions.

The “Big Four” Interfaces

Java provides a set of standard functional interfaces in java.util.function. Mastering these covers 90% of use cases.

Interface Method Signature Purpose Example Use Case
Predicate<T> boolean test(T t) Checks a condition filter()
Function<T, R> R apply(T t) Transforms input to output map()
Consumer<T> void accept(T t) Consumes input, returns nothing forEach()
Supplier<T> T get() Generates a result generate()

4. Interactive: The Interface Matcher

Test your knowledge by matching the Lambda expression to the correct Functional Interface.

Score: 0/5
Lambda Expression
s → s.length()
Which Functional Interface does this implement?

5. Method References

Sometimes a lambda expression does nothing but call an existing method. In those cases, you can use a Method Reference (::) to make code even clearer.

Type Syntax Example Lambda Equivalent
Static Method Class::staticMethod Math::max (a, b) &rarr; Math.max(a, b)
Instance Method (Specific Object) obj::method System.out::println s &rarr; System.out.println(s)
Instance Method (Arbitrary Object) Class::method String::length s &rarr; s.length()
Constructor Class::new ArrayList::new () &rarr; new ArrayList<>()

[!TIP] Use method references when the lambda merely passes its arguments to another method without modification. If you need to manipulate arguments, stick to lambdas.

6. Variable Scope (Effectively Final)

Lambdas can “capture” variables from their surrounding scope. However, local variables used inside a lambda must be final or effectively final (meaning they are assigned only once).

int factor = 2; // Effectively final
Function<Integer, Integer> doubler = n -> n * factor;

// factor = 3; // Compile Error! Variable used in lambda must be final.

This restriction exists because lambdas may run in different threads or at different times, and capturing a mutable local variable could lead to concurrency issues.

7. Hardware Reality: How Lambdas Work

How does Java implement lambdas? Does it create an anonymous inner class for every lambda like in the old days?

No. That would be memory-inefficient (class loading overhead, metaspace usage).

Instead, Java 8 leverages the invokedynamic bytecode instruction (introduced in Java 7).

  1. Compile Time: The compiler generates an invokedynamic call site in the bytecode instead of creating a .class file.
  2. Runtime: When the code is first executed, the JVM calls a Bootstrap Method (LambdaMetafactory).
  3. Optimization: The factory spins up a lightweight class implementation tailored to the lambda. This allows the JVM to optimize the lambda heavily (inlining) and saves memory compared to generating thousands of anonymous classes.

[!NOTE] This is a key example of “Hardware Reality”—optimizing for memory layout and instruction cache by avoiding excessive class loading.


Next Steps

Now that you understand the functional building blocks, let’s see how they power the Stream API to process collections of data.