Functional Programming

This section documents the current Java release line, with Java 25 LTS as the reference point (no specific patch version is pinned), as published at the Java developer portal, the Java Tutorials, and the Java SE API specification — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Java is not a functional language, but since Java 8 it has first-class functions in the form of lambdas and method references, a standard library of function types in java.util.function, and default methods that compose them. Used well, this style replaces mutable loops and branching with small, named, testable pieces of behaviour. This page follows dev.java: Refactoring from the Imperative to the Functional Style and links each type to its java.util.function Javadoc.

The java.util.function Package

Every interface here has a single abstract method (SAM), so a lambda or method reference implements it directly. Prefer these over hand-written functional interfaces. See dev.java: Functional Interfaces.

Interface Abstract method Purpose

Function<T, R>

R apply(T t)

transform one value into another

BiFunction<T, U, R>

R apply(T t, U u)

combine two values into a result

Predicate<T>

boolean test(T t)

a boolean test, e.g. a filter

Consumer<T>

void accept(T t)

perform a side effect, return nothing

Supplier<T>

T get()

produce a value on demand

UnaryOperator<T>

T apply(T t)

a Function whose input and output type match

BinaryOperator<T>

T apply(T a, T b)

a BiFunction of one type; stream reductions

import java.util.function.*;

Function<String, Integer> length         = String::length;
BiFunction<Integer, Integer, Integer> add = Integer::sum;
Predicate<String> isBlank                = String::isBlank;
Consumer<String>  print                  = System.out::println;
Supplier<Long>    now                    = System::currentTimeMillis;
UnaryOperator<String> shout              = s -> s.toUpperCase() + "!";
BinaryOperator<Integer> max              = Integer::max;

length.apply("hello");   // 5
add.apply(2, 3);         // 5
isBlank.test("  ");      // true
shout.apply("hi");       // "HI!"
max.apply(3, 9);         // 9

BiPredicate<T, U> and BiConsumer<T, U> are the two-argument forms of Predicate and Consumer. To avoid boxing in numeric code, use the primitive specializations — IntFunction<R>, ToIntFunction<T>, IntPredicate, IntUnaryOperator, IntBinaryOperator, IntSupplier, IntConsumer (and the Long / Double equivalents):

import java.util.function.*;
import java.util.stream.Stream;

ToIntFunction<String> toLen = String::length;      // returns int, never Integer
IntPredicate even           = n -> n % 2 == 0;
IntUnaryOperator inc        = n -> n + 1;
IntSupplier roll            = () -> (int) (Math.random() * 6) + 1;

int total = Stream.of("a", "bb", "ccc").mapToInt(toLen).sum();   // 6

Composing Behaviour

The default methods on these interfaces build larger behaviour from smaller functions. See dev.java: Combining and Composing Lambdas.

import java.util.*;
import java.util.function.*;

Function<Integer, Integer> times2 = n -> n * 2;
Function<Integer, Integer> plus1  = n -> n + 1;

times2.andThen(plus1).apply(10);   // (10 * 2) + 1 = 21   -- this, then the argument
times2.compose(plus1).apply(10);   // (10 + 1) * 2 = 22   -- the argument, then this

Predicate<String> nonNull  = Objects::nonNull;
Predicate<String> nonEmpty = s -> !s.isEmpty();
Predicate<String> usable   = nonNull.and(nonEmpty);
Predicate<String> unusable = usable.negate();
usable.or(s -> s.equals("?")).test("hi");   // true

Consumer<String> logIt   = s -> System.out.println("log: " + s);
Consumer<String> storeIt = s -> System.out.println("store: " + s);
logIt.andThen(storeIt).accept("event");     // runs both, in order

Comparator<String> byLength = Comparator.comparingInt(String::length);
Comparator<String> ordering = byLength.thenComparing(Comparator.naturalOrder()).reversed();

Function.identity(), Predicate.not(…​), and UnaryOperator.identity() round out the toolkit. Comparator composes the same way — comparing, thenComparing, reversed — and is covered in full on Interfaces.

Higher-Order Functions and Closures

A higher-order function takes a function as a parameter, returns one, or both.

import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;

// takes a function
static <T, R> List<R> mapEach(List<T> in, Function<? super T, ? extends R> f) {
    return in.stream().map(f).collect(Collectors.toList());
}

// returns a function that closes over 'base'
static Function<Integer, Integer> adder(int base) {
    return n -> n + base;
}

Function<Integer, Integer> plus10 = adder(10);
plus10.apply(5);                                  // 15
mapEach(List.of("a", "bb"), String::length);     // [1, 2]

Currying rewrites a two-argument function as a chain of one-argument functions; partial application fixes some arguments now and takes the rest later:

import java.util.function.*;

// curried: Integer -> (Integer -> Integer)
Function<Integer, Function<Integer, Integer>> add = a -> b -> a + b;
add.apply(3).apply(4);                            // 7

// partial application of a BiFunction
BiFunction<Integer, Integer, Integer> rawAdd = Integer::sum;
Function<Integer, Integer> add5 = b -> rawAdd.apply(5, b);
add5.apply(10);                                   // 15

A lambda closes over the local variables it reads, which must be final or effectively final — assigned exactly once. A loop counter that is reassigned each iteration therefore cannot be captured directly; use a stream, a single-element array, or AtomicInteger instead.

import java.util.function.UnaryOperator;

int factor = 3;                                   // effectively final
UnaryOperator<Integer> scale = n -> n * factor;
// factor = 4;                                    // uncommenting this stops the lambda compiling
scale.apply(10);                                  // 30

From Imperative Loop to Pipeline

The dev.java refactoring guide reworks statement-by-statement loops into declarative pipelines. Before — an accumulator, nested conditionals, a trailing sort:

import java.util.*;

static List<String> namesOfAdults(List<Person> people) {
    List<String> result = new ArrayList<>();
    for (Person p : people) {
        if (p.age() >= 18) {
            String name = p.name().toUpperCase();
            if (!result.contains(name)) {
                result.add(name);
            }
        }
    }
    Collections.sort(result);
    return result;
}

After — the same logic as a filter / map / distinct / sorted pipeline, each step named:

import java.util.List;

static List<String> namesOfAdults(List<Person> people) {
    return people.stream()
            .filter(p -> p.age() >= 18)
            .map(p -> p.name().toUpperCase())
            .distinct()
            .sorted()
            .toList();
}

The pipeline states what to compute, keeps no mutable accumulator, and reads top to bottom. Keep an explicit loop when a step needs an early return / break, has to throw a checked exception, or fills several result collections in one pass.

See Also