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 |
|---|---|---|
|
|
transform one value into another |
|
|
combine two values into a result |
|
|
a boolean test, e.g. a filter |
|
|
perform a side effect, return nothing |
|
|
produce a value on demand |
|
|
a |
|
|
a |
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
-
Lambdas and Method References — the syntax and capture rules for every function value on this page.
-
Streams and Collectors — where these function types are consumed in practice.
-
Interfaces —
@FunctionalInterface, the single-abstract-method rule, andComparatorcomposition. -
Methods and Parameters — method references as a shorthand for calling existing methods.