Phase 3 · Core JavaModule 17~40 min read

Functional Programming & Lambdas

Treat behavior as data with lambda expressions, functional interfaces, and method references.

What you'll learn

This module changes how you write Java. Functional programming treats behaviour as data — you can store a piece of logic in a variable, pass it to a method, and return it from one. Lambdas make this concise, and they're the foundation of the powerful Stream API coming next.

By the end you'll be able to:

  • Understand what functional programming brings to Java
  • Write lambda expressions fluently
  • Use the built-in functional interfaces (Predicate, Function, Consumer, Supplier)
  • Shorten lambdas with method references
  • Compose and pass behaviour with higher-order functions

The idea: behaviour as data

Until now, data (numbers, strings, objects) went into methods and results came out. Functional programming adds a twist: the behaviour itself can be a value. Instead of writing a whole class to say "print this" or "is this even," you capture that logic in a lambda and hand it around like any other object. The result is code that's shorter, more flexible, and easier to read.

Lambda syntax

A lambda is an anonymous function — parameters, an arrow ->, and a body. It's a compact stand-in for a functional interface (Module 10). Compare the old anonymous class with the lambda:

Lambda.java
// Before Java 8: a clunky anonymous class
Runnable oldWay = new Runnable() {
    public void run() { System.out.println("Hello"); }
};

// With a lambda: the same thing, distilled
Runnable newWay = () -> System.out.println("Hello");

oldWay.run();
newWay.run();
Anatomy of a lambda
(a, b) -> a + b

Parameters

The inputs (types are usually inferred).

Arrow

Separates inputs from the body.

Body

The expression or block that runs.

Note

Shortcuts: with one parameter you can drop the parentheses (x -> x * 2); with a single expression you can drop the braces and return. For a multi-line body, use { ... return ...; }.

The built-in functional interfaces

You rarely need to define your own functional interface — java.util.function provides the common shapes. Learn these four and you'll recognise them everywhere in modern Java:

InterfaceMethodShapeMeans
Predicate<T>testT -> booleana yes/no test
Function<T,R>applyT -> Rtransform T into R
Consumer<T>acceptT -> voiddo something with T
Supplier<T>get() -> Tproduce a T
Also: UnaryOperator<T> (T -> T) and BinaryOperator<T> ((T, T) -> T).
Functions.java
import java.util.function.*;

Predicate<Integer> isEven  = n -> n % 2 == 0;   // T  -> boolean
Function<String, Integer> len = s -> s.length(); // T  -> R
Consumer<String> printer = s -> System.out.println("-> " + s); // T -> void
Supplier<String> greet   = () -> "Hello!";      // () -> T

System.out.println(isEven.test(4));     // true
System.out.println(len.apply("Java"));  // 4
printer.accept("hi");                   // -> hi
System.out.println(greet.get());        // Hello!

Method references

When a lambda does nothing but call one existing method, a method reference (::) says the same thing even more concisely. x -> System.out.println(x) becomes System.out::println:

MethodRef.java
import java.util.*;

List<String> names = new ArrayList<>(List.of("charlie", "alice", "bob"));

names.forEach(System.out::println);   // method reference (vs x -> println(x))
names.sort(String::compareTo);        // reference to an instance method
System.out.println(names);

Note

Four forms exist: static (Integer::parseInt), bound instance (System.out::println), unbound instance (String::toUpperCase), and constructor (ArrayList::new). Use one whenever a lambda just forwards to a method.

Higher-order functions

A higher-order function takes or returns another function. This is what makes functional code so composable — you can build complex behaviour by chaining simple pieces. Function offers andThen and compose; Predicate offers and, or, and negate:

HigherOrder.java
import java.util.function.Function;

Function<Integer, Integer> doubler   = x -> x * 2;
Function<Integer, Integer> increment = x -> x + 1;

// compose: run doubler, then increment
Function<Integer, Integer> combined = doubler.andThen(increment);
System.out.println(combined.apply(5));   // (5 * 2) + 1

Effectively final

A lambda can use local variables from its surrounding scope, but only if they are effectively final — assigned once and never changed. Try to modify such a variable inside or after the lambda and the compiler complains. (Fields don't have this restriction.)

Recap & quick check

Key takeaways

  • Functional programming treats behaviour as data you can store, pass, and return.
  • A lambda is a concise anonymous function: (params) -> body, backed by a functional interface.
  • Learn Predicate (test), Function (apply), Consumer (accept), Supplier (get).
  • Method references (Class::method) shorten lambdas that just call one method.
  • Higher-order functions take/return functions; captured local variables must be effectively final.

Quick check

1. What is a lambda expression?

2. Which functional interface represents a T -> boolean test?

3. What does System.out::println represent?

4. A local variable used inside a lambda must be…

5. What does Function's andThen do?

Excellent — you're thinking functionally now. Next up: Module 18 — the Stream API, where lambdas truly shine.