☕ Java

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.

What Makes a Function Pure: Determinism and No Side Effects

A function is pure when it satisfies two independent properties at once. The first is determinism: given the same input arguments, the function always returns the same result, no matter how many times it's called, in what order, or what else has happened in the program in the meantime — the output is a function of the input arguments alone, never of any hidden state the caller can't see in the function's signature. The second is the absence of side effects: the function does not mutate any object passed into it, does not read or write any static or instance field outside its own local variables, does not perform I/O (printing, file access, network calls, database queries), and does not throw an exception whose occurrence depends on something other than the input arguments themselves. These two properties together give rise to referential transparency: any call to a pure function can be replaced, anywhere in the source code, by the value it returns, without changing the behavior of the surrounding program in any way — because the call has no effect other than producing that value, and the value is guaranteed to be the same every time. This is the property that lets a developer reason about a pure function in isolation, the way one reasons about a mathematical function: square applied to four is sixteen, full stop, regardless of when or how many times it's evaluated, or what else the program is doing. Most simple arithmetic and string-manipulation methods are naturally pure, because their inputs are immutable value types and their results are newly computed values rather than mutations.
Java
// ── Pure functions: output depends only on input, no observable effects ─
static int add(int a, int b) {
    return a + b;                      // deterministic, no side effects
}

static int square(int x) {
    return x * x;                      // same, every time
}

static String greet(String name) {
    return "Hello, " + name + "!";     // builds and returns a NEW String
}

// Calling these repeatedly with the same arguments always gives the same result,
// and the call can be replaced by its result anywhere without changing behavior:
int a = square(5);   // a == 25
int b = square(5);   // b == 25, always, forever
// square(5) could be replaced by the literal 25 everywhere in this program
// without changing what the program does — that's referential transparency.

// ── A function that LOOKS pure but isn't, because of a subtle dependency ─
static int offset = 10;

static int impureLookingButReallyIsnt(int x) {
    return x + offset;     // depends on a field that lives outside this
                             // method — NOT pure, even though it takes one
                             // argument and returns a value
}
// if offset changes, the same call with x=5 returns a different result
// each time — not deterministic

Common Sources of Impurity in Java Code

Impurity creeps into Java code through a handful of recurring patterns, often without the developer noticing, because Java's object references make it easy to mutate state that lives outside a method's own local variables. Reading or writing a static or instance field is the most common source: a method that increments a counter field, appends to a shared log, or reads a configuration field that can change at runtime is not pure, because its behavior depends on, or alters, state the caller cannot see by looking at the method's parameters alone. Mutating an argument is a related but distinct trap: Java passes object references by value, which means a method cannot reassign the caller's reference, but it absolutely can mutate the fields of the object the reference points to — and the caller will see that mutation reflected in the very object it passed in, which is a side effect even though no field of the method's own class was touched. Performing I/O of any kind is inherently impure, both because it has an observable effect on the outside world and because the result of, say, a network call is not deterministic in the way pure functions require. Calling a source of non-determinism such as a random number generator or the system clock breaks determinism directly, even with no other side effect present, because the same call produces a different value each time it's invoked, by design. Distinguishing genuinely pure code from code that merely looks pure usually comes down to asking two questions: does the return value depend on anything other than the parameters, and does evaluating this method change anything that another part of the program, called before or after, could observe?
Java
// ── Source of impurity #1: reading/writing static or instance fields ───
class RunningTotal {
    private int total = 0;

    // IMPURE: mutates instance state, return value depends on call history
    int addAndGetTotal(int amount) {
        total += amount;
        return total;     // same argument, different result each call
    }
}

class PureTotal {
    // PURE: takes the previous total explicitly, returns a NEW total,
    // mutates nothing — the caller decides whether/how to keep track
    static int addToTotal(int previousTotal, int amount) {
        return previousTotal + amount;
    }
}

// ── Source of impurity #2: mutating an argument passed by reference ────
static void clearAndFillImpure(List<String> list, String value) {
    list.clear();          // mutates the CALLER's list — visible side effect
    list.add(value);
}

static List<String> fillPure(List<String> original, String value) {
    List<String> copy = new ArrayList<>();
    copy.add(value);
    return copy;            // returns a NEW list, leaves original untouched
}

// ── Source of impurity #3: I/O ──────────────────────────────────────────
static int impureLoggingAdd(int a, int b) {
    int result = a + b;
    System.out.println("Computed: " + result);  // observable side effect (console)
    return result;
}
// A pure version simply omits the println — logging, if needed, is the
// CALLER's responsibility, kept outside the pure computation.

// ── Source of impurity #4: non-deterministic inputs ─────────────────────
static long impureTimestampedId(String name) {
    return name.hashCode() + System.currentTimeMillis();  // different every call
}
static long pureId(String name, long timestamp) {
    return name.hashCode() + timestamp;   // deterministic — timestamp is now an input
}

Why Purity Matters: Testability, Memoization, and Safe Parallelism

Pure functions are easier to test than impure ones because a test for a pure function needs nothing more than a set of inputs and the expected output — there is no setup of mock objects, no need to reset shared state between test cases, no concern about test execution order affecting results, and no need to verify that some side effect happened in addition to checking the return value. An impure function, by contrast, often needs its surrounding state arranged just right before each test and inspected afterward, because the function's behavior is entangled with that state. Because a pure function always returns the same output for the same input, its results can be safely cached — a technique called memoization — without any risk of serving a stale or incorrect result: if the input has already been seen, the previously computed output is still correct by definition, since nothing the function depends on can have changed. This optimization is unsound for an impure function, since its result for the "same" input may legitimately differ depending on when it's called. Pure functions are also automatically safe to run in parallel, because by definition they touch no mutable state outside their own local variables — there's nothing for two concurrently executing calls to race over. This is precisely the assumption the JDK's parallel Stream operations make about the functions passed into map, filter, and reduce: the contract for these methods specifies that the supplied functions should be stateless and non-interfering, and supplying an impure lambda to a parallel stream produces results that depend on timing and thread scheduling, which is a frequent and hard-to-diagnose source of concurrency bugs. A common practical strategy for capturing these benefits without giving up on side effects altogether — since a real program eventually needs to print, save, and communicate with the outside world — is to structure code as a "functional core, imperative shell": the bulk of the program's logic lives in pure functions that take inputs and compute outputs with no side effects, while a thin outer layer handles the unavoidable I/O, calling into the pure core for everything that can be expressed as plain computation.
Java
// ── Testability: a pure function needs no setup or mocks ───────────────
static int discountedPrice(int price, double discountRate) {
    return (int) Math.round(price * (1 - discountRate));
}
// Test is just input -> expected output, nothing else to arrange:
assert discountedPrice(100, 0.20) == 80;

// ── Memoization: safe ONLY because the wrapped function is pure ────────
static <T, R> Function<T, R> memoize(Function<T, R> pureFunction) {
    Map<T, R> cache = new ConcurrentHashMap<>();
    return input -> cache.computeIfAbsent(input, pureFunction);
    // Safe because pureFunction(input) is guaranteed to return the same
    // result every time — caching it can never serve a stale answer.
}

Function<Integer, Long> slowFibonacci = n -> {
    if (n <= 1) return (long) n;
    long a = 0, b = 1;
    for (int i = 2; i <= n; i++) { long next = a + b; a = b; b = next; }
    return b;
};
Function<Integer, Long> fastFibonacci = memoize(slowFibonacci);
fastFibonacci.apply(40);   // computed once
fastFibonacci.apply(40);   // served instantly from cache — correct because pure

// ── Safe parallelism: no shared mutable state for threads to race on ───
List<Integer> numbers = IntStream.rangeClosed(1, 1_000_000).boxed().toList();

// SAFE in parallel: squaring is pure, no shared state touched:
long sumOfSquares = numbers.parallelStream()
        .mapToLong(n -> (long) n * n)   // pure — depends only on n
        .sum();

// UNSAFE in parallel: mutates a shared, non-thread-safe collection —
// a classic case of supplying an impure (stateful, interfering) lambda
// where the Stream API's contract requires a pure one:
List<Integer> sharedResults = new ArrayList<>();   // not thread-safe
numbers.parallelStream().forEach(n -> sharedResults.add(n * n));
// Result: may throw, may silently drop elements, may corrupt internal state —
// behavior depends on thread timing, which is exactly what purity avoids.

// ── Functional core, imperative shell ───────────────────────────────────
record Order(String product, int price, double discount) {}

// Pure core: all real logic, fully testable, no I/O:
static List<String> formatInvoiceLines(List<Order> orders) {
    return orders.stream()
            .map(o -> o.product() + ": $" + discountedPrice(o.price(), o.discount()))
            .toList();
}

// Imperative shell: the only place I/O happens, kept as thin as possible:
static void printInvoice(List<Order> orders) {
    formatInvoiceLines(orders).forEach(System.out::println);  // side effect isolated here
}

Related Topics in Functional Programming

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.
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.