Functional Programming Basics
Functional programming is a programming paradigm that treats computation as the evaluation and composition of functions, favors expressions that produce values over statements that mutate state, and prefers passing behavior around as data rather than encoding it only inside class hierarchies. Java was designed from the outset as an object-oriented, imperative language: methods could be invoked but never treated as values — they could not be stored in a variable, passed as an argument, or returned from another method, except through the verbose workaround of wrapping a single method inside an anonymous class that implemented a one-method interface. Java 8 closed this gap by introducing lambda expressions, method references, and a standard library of single-method functional interfaces in java.util.function, together with the Stream API built on top of them, giving Java a genuinely functional layer on top of its existing object-oriented core without abandoning that core. This entry covers what distinguishes functional style from imperative and object-oriented style, how Java represents a function as an instance of a functional-interface type rather than as a true first-class value, the standard functional interfaces the JDK provides for common shapes of behavior, and how adopting functional style changes the shape of everyday Java code.
What Functional Programming Is — and What Java Was Missing
// ── Before Java 8: behavior passed as an anonymous class ───────────────
List<String> names = new ArrayList<>(List.of("Wanda", "Ed", "Carlos", "Ana"));
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
});
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Running in a background thread");
}
}).start();
// ── Java 8+: the same behavior expressed as a lambda expression ───────
Collections.sort(names, (a, b) -> Integer.compare(a.length(), b.length()));
new Thread(() -> System.out.println("Running in a background thread")).start();
// ── Even shorter: a method reference, when an existing method matches ─
names.sort(Comparator.comparingInt(String::length));
// String::length is a method reference — it refers to the instance method
// length() on String, without invoking it; the compiler turns it into an
// implementation of ToIntFunction<String> wherever that type is expected.
// ── The underlying interfaces did not change — only the call-site syntax did
// Comparator<String> is still an interface with one abstract method, compare();
// a lambda or method reference is compiled into an object that implements it.
interface Comparator<T> {
int compare(T o1, T o2);
// default and static methods such as reversed(), thenComparing(),
// comparingInt() exist too, but they don't count toward the "single
// abstract method" requirement that makes this a functional interface.
}Functional Interfaces — How Java Represents a Function as a Value
// ── A custom functional interface ──────────────────────────────────────
@FunctionalInterface
interface Transformer<T, R> {
R transform(T input);
// Adding a SECOND abstract method here would break compilation,
// because @FunctionalInterface requires exactly one.
// Default methods are allowed without affecting SAM status:
default Transformer<T, R> logged() {
return input -> {
R result = transform(input);
System.out.println(input + " -> " + result);
return result;
};
}
}
Transformer<String, Integer> wordLength = String::length; // method reference
Transformer<String, Integer> loud = wordLength.logged();
loud.transform("functional"); // prints "functional -> 10", returns 10
// ── Target typing: the same lambda body, different target interfaces ──
UnaryOperator<Integer> doubleAsUnary = x -> x * 2; // T -> T shape
Function<Integer, Integer> doubleAsFunction = x -> x * 2; // T -> R shape
Transformer<Integer, Integer> doubleAsCustom = x -> x * 2; // custom SAM shape
// All three hold "functionally" the same behavior, but as instances of
// three different interface types — the lambda itself has no standalone type.
// ── The four forms of method reference ─────────────────────────────────
Function<String, Integer> parse = Integer::parseInt; // static method
Supplier<String> greeting = "hello"::toUpperCase; // bound instance method
Function<String, Integer> length = String::length; // unbound instance method
// (first arg becomes the receiver)
Supplier<ArrayList<String>> listFactory = ArrayList::new; // constructor reference
System.out.println(parse.apply("42")); // 42
System.out.println(greeting.get()); // HELLO
System.out.println(length.apply("Java")); // 4
List<String> fresh = listFactory.get(); // a new, empty ArrayList<String>The java.util.function Toolkit and Functional Style in Everyday Code
// ── The core java.util.function shapes ──────────────────────────────────
Function<String, Integer> toLength = String::length; // T -> R
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b; // (T,U) -> R
Supplier<Double> randomVal = Math::random; // () -> T
Consumer<String> printer = System.out::println; // T -> void
BiConsumer<String, Integer> logPair = (k, v) -> System.out.println(k + "=" + v);
Predicate<String> isBlank = String::isBlank; // T -> boolean
UnaryOperator<String> shout = String::toUpperCase; // T -> T
BinaryOperator<Integer> max = Integer::max; // (T,T) -> T
// ── Primitive specializations avoid boxing in hot paths ────────────────
IntPredicate isEven = n -> n % 2 == 0;
IntUnaryOperator square = n -> n * n;
IntBinaryOperator sum = (a, b) -> a + b;
ToIntFunction<String> len = String::length; // T -> int, no Integer boxing
// ── Rewriting an imperative loop in functional style ────────────────────
List<String> words = List.of("alpha", "beta", "gamma", "delta", "epsilon");
// Imperative: explicit loop, mutable accumulator, manual branching:
int totalLengthImperative = 0;
for (String w : words) {
if (w.length() > 4) {
totalLengthImperative += w.length();
}
}
// Functional: a declarative pipeline of small, composed steps:
int totalLengthFunctional = words.stream()
.filter(w -> w.length() > 4) // Predicate<String>
.mapToInt(String::length) // ToIntFunction<String>
.sum(); // reduction, no mutable accumulator visible
System.out.println(totalLengthImperative); // 22 (alpha=5, gamma=5, delta=5, epsilon=7)
System.out.println(totalLengthFunctional); // 22 — same result, computed declaratively
// ── Optional.map / orElse replacing explicit null branching ────────────
Optional<String> maybeName = Optional.of("functional");
// Imperative null-check style:
String resultImperative;
if (maybeName.isPresent()) {
resultImperative = maybeName.get().toUpperCase();
} else {
resultImperative = "UNKNOWN";
}
// Functional style: behavior passed in as a Function, branching hidden inside Optional:
String resultFunctional = maybeName.map(String::toUpperCase).orElse("UNKNOWN");