☕ Java

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

Functional programming organizes a program around functions that take inputs and produce outputs, composed together to build larger computations, rather than around sequences of statements that mutate variables and object state over time. Three ideas recur across functional languages and the parts of Java that adopt this style: functions are treated as values that can be passed, stored, and returned just like numbers or strings (first-class functions); programs are built by composing small functions into larger ones rather than by writing long procedural sequences; and code favors producing new values over mutating existing state, since immutable data is easier to share safely and reason about. This sits alongside, rather than replacing, Java's two older paradigms — imperative programming, where a program is a sequence of statements that change variables and object fields step by step, and object-oriented programming, where state and the behavior that operates on it are bundled together inside objects and exposed through methods. Before Java 8, Java had no syntax for treating a method as a value in its own right. A method could only be invoked through an object reference; it could not be assigned to a variable, passed into another method as an argument, or returned as a result. The workaround predates Java 8 by over a decade: define a single-method interface — Runnable, Comparator, ActionListener, and dozens of others in the JDK and in user code followed this shape — and instantiate it with an anonymous class whose one method holds the actual behavior. This worked, but the ceremony of writing a multi-line anonymous class just to pass a five-line comparison routine into a sort call buried the actual logic in boilerplate, and discouraged writing new higher-order-style APIs of one's own, since asking callers to write anonymous classes for every use felt like an unreasonable burden. The pressure to fix this became concrete with the design of the Stream API for Java 8: processing a collection functionally — filtering, transforming, and reducing elements — requires being able to pass a small piece of behavior (the filter condition, the transformation, the combining operation) into a library method concisely. Lambda expressions were introduced specifically to make this kind of call site readable, and method references were added alongside them so that an existing method could be supplied directly as that behavior, without even writing a lambda, when its signature already matched what was needed.
Java
// ── 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 functional interface is any interface that declares exactly one abstract method, regardless of how many default or static methods it also declares — the single abstract method (often abbreviated SAM) is the one piece of behavior that a lambda expression or method reference supplies when it is used as that interface's implementation. The compiler enforces this when the @FunctionalInterface annotation is present: if an interface annotated this way ends up with zero or more than one abstract method, compilation fails with an explicit error, which makes the annotation useful as a guard against accidentally breaking an interface's status as a valid lambda target while editing it — though the annotation is documentation and a safety check, not what makes an interface "functional" in the first place; an interface with exactly one abstract method is a valid lambda target whether or not it carries the annotation. A lambda expression itself is not, strictly speaking, a free-floating function value the way it would be in a language with true first-class functions — it is shorthand that the compiler converts, at each location it's used, into an instance of whatever functional-interface type is expected at that location, implementing that interface's single abstract method with the lambda's body. This is called target typing: the same lambda expression could become a UnaryOperator<Integer>, a custom interface, or any other single-method interface whose method shape is compatible, depending entirely on what type the surrounding context expects. The lambda has no type of its own until it's assigned, passed, or returned into a context that supplies one. Method references are an alternative, often more concise, way to supply a functional interface's implementation when a method that already exists matches the required signature exactly — there are four forms: a reference to a static method, a reference to an instance method on a particular object, a reference to an instance method to be invoked on whichever object is supplied as the functional interface's first parameter, and a reference to a constructor (used to supply a Supplier or Function whose job is to create new objects).
Java
// ── 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

Rather than asking developers to declare a new functional interface for every shape of behavior they need, the JDK ships java.util.function with general-purpose interfaces covering the common shapes: Function<T,R> takes one argument and produces a result of a possibly different type; BiFunction<T,U,R> takes two arguments; Supplier<T> takes nothing and produces a value (useful for lazy or deferred computation, or for factories); Consumer<T> takes a value and produces no result, used purely for its side effect; BiConsumer<T,U> is the two-argument version; Predicate<T> takes a value and returns a boolean, used for filtering and testing conditions; UnaryOperator<T> is a Function<T,T> specialization where the input and output type are the same; and BinaryOperator<T> is the two-argument, same-type version used for combining two values into one of the same type, heavily used in reduction operations like a sum or max. Primitive specializations — IntFunction, ToIntFunction, IntPredicate, IntUnaryOperator, IntBinaryOperator, and their long/double counterparts — exist purely to avoid the performance cost of boxing primitives into wrapper objects every time a primitive value flows through one of these interfaces. Adopting these interfaces changes the shape of ordinary code: a task that previously needed a loop with a mutable accumulator and explicit control flow can instead be expressed as a pipeline of small functions chained together, each one expressing a single transformation or filter step, with the looping and accumulation handled internally by a library method (most visibly, the Stream API's map, filter, and reduce). The same shift shows up with Optional: instead of writing an explicit null check and branching imperatively, Optional's map and ifPresent methods accept a Function or Consumer describing what to do with the value if it is present, leaving the presence-or-absence branching to the Optional implementation itself. This style tends to push side effects toward the edges of a computation — typically inside a final forEach or a Consumer at the very end of a pipeline — while the steps in between remain side-effect-free transformations, a separation that becomes the basis for the Pure Functions entry in this series.
Java
// ── 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");

Related Topics in Functional Programming

Pure Functions
A pure function is a function whose return value depends solely on the arguments passed to it, with no dependence on any hidden or external state, and which produces no observable effect on the world beyond computing and returning that value — no mutation of arguments, no writes to shared state, no I/O, and no dependence on anything that can change between calls such as the system clock or a random number generator. Calling a pure function with the same arguments always produces the same result, a property known as referential transparency, which means a call to a pure function can be replaced anywhere in a program by its result without changing what the program does. Java does not enforce purity at the language level — any method, lambda, or method reference may freely read and write shared mutable state, perform I/O, or behave non-deterministically — so purity in Java is a discipline the developer chooses to follow rather than a guarantee the compiler provides. This entry covers the precise definition of purity and referential transparency, the common ways Java code becomes impure even unintentionally, and the practical payoffs of writing pure functions: simpler testing, safe caching and memoization, and safe parallel execution.
Higher Order Functions
A higher-order function is a function that takes one or more other functions as parameters, returns a function as its result, or both — as opposed to a first-order function, which only takes and returns plain data values like numbers, strings, or objects. Java has supported a narrow form of this since its very first versions, since passing a Runnable to a Thread or a Comparator to Collections.sort is technically passing a function (wrapped in a single-method interface) into another method, but writing an anonymous class at every call site made the pattern feel like a special case reserved for a handful of built-in APIs rather than a general technique available for everyday code. Lambda expressions and method references, introduced in Java 8, made supplying a function as an argument concise enough that higher-order functions became a natural, everyday tool — most visibly through the Stream API's map, filter, and reduce, but equally usable in ordinary application code through java.util.function. This entry covers the definition of a higher-order function, how Java represents functions as arguments and return values without true first-class functions, the most common shapes of functions that take functions as parameters, and the complementary shape of functions that build and return other functions, including function composition.
Closures
A closure is a function bundled together with the variables from its enclosing lexical scope that it refers to, captured at the time the function is created, so that the function can keep using those variables even after the scope that originally held them has finished executing. In Java, both lambda expressions and anonymous inner classes form closures: they can refer to local variables and parameters of the method that creates them, and to the fields of the enclosing object, even after that method has returned and its stack frame is gone. Java places one significant restriction on this that languages like JavaScript do not: any local variable or parameter captured by a lambda or inner class must be effectively final — assigned exactly once and never reassigned — which rules out the kind of closure-over-a-mutable-counter pattern that is idiomatic in other languages, and requires a small workaround whenever genuinely mutable shared state is needed. This entry covers what a closure captures and how, why Java enforces the effectively-final rule, the standard workarounds for needing mutable captured state, and the practical patterns and memory implications of using closures in real code.
Currying
Currying is the technique of transforming a function that accepts multiple arguments into a chain of functions that each accept exactly one argument, so that calling the first function with the first argument returns a new function waiting for the second argument, and so on until every argument has been supplied and a final result is produced — a two-argument function becomes a one-argument function that, given the first argument, returns another one-argument function that, given the second, produces the result. This is distinct from partial application, which means fixing some subset of a function's arguments to produce a new function over the rest, since partial application does not require restructuring a function into a one-argument-at-a-time chain and can be done directly on an ordinary multi-argument function. Languages such as Haskell curry every function by default, with no special syntax needed to call a curried function one argument at a time; Java has no built-in support for currying, so a curried function must be constructed by hand as a chain of nested functional interfaces, most often Function<A, Function<B, R>> for two arguments, nested one layer deeper for each additional argument. This entry covers the precise definition of currying and how it differs from partial application, how to build curried functions in Java using nested Function types and a reusable curry helper, and the practical situations where currying earns its verbosity in Java versus situations where a simpler approach reads better.