Course contents

Functional programming in Java

Pure functions, immutability, higher-order functions, referential transparency, composition and currying, laziness with Supplier and streams, recursion without tail calls, side-effect pitfalls in streams, and an honest look at streams versus loops.

PremiumOften askedLesson17 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: a calculator versus a chef in your kitchen
  2. Functional ideas under the hood
  3. Real world: imperative versus declarative, and side effects in streams
  4. Advanced: where Java is and isn't functional
  5. How to say it in the interview

🧮 What is functional programming in Java?

Functional programming is a style where you build programs out of functions that take values and return new values without changing anything else. Instead of a loop that keeps adding to a total variable, you describe the result: orders.stream().filter(Order::isPaid).mapToLong(Order::cents).sum(). Java isn't a functional language, but since Java 8 it supports the style well enough that most modern backend code mixes the two.

When an interviewer asks "what is functional programming?", they rarely want a lecture on lambda calculus. They want to know whether you understand why the style helps (code that's easier to test, reason about and parallelise), whether you can name its building blocks, and, most of all, whether you know where it breaks down in Java: side effects inside streams, recursion without tail calls, and streams that are slower than the loop they replaced.

What you’ll learn

  • The core ideas of functional programming: pure functions, immutability, referential transparency and higher-order functions, each with Java code
  • How to compose functions and curry them with Function<A, Function<B, C>>, and when that's worth the ceremony
  • How laziness works with Supplier and stream pipelines, and why a stream with no terminal operation does nothing
  • Why deep recursion throws StackOverflowError in Java (no tail-call optimisation) and what to do instead
  • The side-effect pitfalls in streams (shared mutable state, peek, parallel forEach) and an honest performance comparison of streams and loops

For this lesson, we'll focus on the core ideas (pure functions, immutability, higher-order functions, referential transparency), composition and currying, laziness, recursion, side-effect pitfalls in streams, the imperative versus declarative trade-off, and an honest performance comparison.

Below the line (out of scope)

The 30-second answer

Functional programming means writing pure functions (output depends only on inputs, no side effects), working with immutable data, and treating functions as values you can pass, return and compose. Pure code is referentially transparent: you can replace a call with its result without changing behaviour, which makes it trivial to test, cache and run in parallel. Java supports this through lambdas, java.util.function, streams, records and List.of, and evaluation in streams is lazy. But Java has no tail-call optimisation, so deep recursion overflows the stack, and nothing stops you from mutating shared state inside a lambda. My default is functional style for data transformations and plain loops when the logic is stateful or performance-critical.

Interview radar

Asked very often: What is a pure function, and what does functional programming look like in Java with lambdas and streams? Senior rounds probe: Why side effects inside streams are dangerous, how lazy evaluation works, and why Java has no tail-call optimisation. Production angle: A parallelStream().forEach that adds to a shared ArrayList and loses elements or throws under load.

01Picture it first: a calculator versus a chef in your kitchen

A pure function is like a calculator. Press 1000 × 0.9 and you get 900, today, tomorrow, on anyone's calculator, and the calculator doesn't rearrange your desk while doing it. An impure function is like a chef you invite into your kitchen: you ask for an omelette, and you get one, but the result depends on what's in your fridge right now, and afterwards your eggs are gone and the pans have moved.

Neither is wrong; programs need side effects eventually, or they'd do nothing. The functional idea is to keep the calculator part as large as possible and push the kitchen part (database writes, clocks, network calls) to the edges, where you can see it.

Here's the difference for a pricing function; watch the hidden arrows on the right.

Fig. 1

Two panels: a pure finalPrice takes cents and a date and returns 900, with nothing else touched; an impure finalPrice takes an order but also secretly reads the system clock, increments a counter field and writes a log line, so the same input can give different outputs

Two panels: a pure finalPrice takes cents and a date and returns 900, with nothing else touched; an impure finalPrice takes an order but also secretly reads the system clock, increments a counter field and writes a log line, so the same input can give different outputs

02Functional ideas under the hood

Pure functions and referential transparency

Try it yourself first

Before reading on: how would you write a unit test proving that the Friday discount works, for each version below?

// Impure: depends on the clock and changes shared state
class DiscountService {
    private int discountsGiven = 0;

    long finalPrice(Order order) {
        boolean friday = LocalDate.now().getDayOfWeek() == DayOfWeek.FRIDAY;
        if (friday) discountsGiven++;
        return friday ? order.cents() * 90 / 100 : order.cents();
    }
}

// Pure: everything it depends on arrives as a parameter
static long finalPrice(long cents, LocalDate today) {
    return today.getDayOfWeek() == DayOfWeek.FRIDAY ? cents * 90 / 100 : cents;
}

finalPrice(1_000, LocalDate.of(2026, 10, 16));   // a Friday: prints 900

For the impure version you need to fake the system clock or wait until Friday, and the counter means two calls in a row don't leave the object in the same state. The pure version takes a date, so the test is one line.

Notice three things:

  • A pure function has two properties: the same inputs always give the same output, and calling it has no observable effect (no field writes, no I/O, no exceptions thrown on valid input).
  • Referential transparency follows from purity: finalPrice(1_000, friday) can be replaced by 900 anywhere in the program without changing behaviour. That's what makes caching (memoisation) and reordering safe.
  • You don't need a framework to get this. Passing LocalDate or a java.time.Clock in, instead of reading LocalDate.now() inside, is the whole trick.

Immutability

Pure functions are much easier to keep pure when the data they receive can't change under them. Java gives you three tools: final fields, records (shallowly immutable by design), and unmodifiable collections like List.of and List.copyOf.

record Cart(String userId, List<LineItem> items) {
    Cart {
        items = List.copyOf(items);              // defensive, unmodifiable copy
    }

    Cart add(LineItem item) {                    // "change" = return a new Cart
        var next = new ArrayList<>(items);
        next.add(item);
        return new Cart(userId, next);
    }

    long totalCents() {
        return items.stream().mapToLong(LineItem::cents).sum();
    }
}

cart.add(item) leaves cart untouched and returns a new one, so any thread holding the old cart sees a consistent snapshot without locks. The cost is a copy per change, which is fine for a 20-line cart and wrong for a 2-million-entry index; persistent collections (Vavr, PCollections) share structure to make that cheaper, but the JDK doesn't ship them.

Watch the difference between unmodifiable and immutable. Collections.unmodifiableList(list) is a read-only view: if someone still holds list, they can change it and your "immutable" view changes too. List.copyOf takes a real copy. And a record holding a List<LineItem> is only as immutable as LineItem.

First-class and higher-order functions

Premium lesson

Keep reading with the Java Pass

The rest of this lesson is part of Java One-Stop Interview Prep: in-depth explanations, diagrams, model answers and how to say them in the interview.

  • Real world: imperative versus declarative, and side effects in streams
  • Advanced: where Java is and isn't functional
  • How to say it in the interview

From ₹199 · one-time payment · No auto-renewal

Practise this topicFunctional Java basics: interview questions & scenarios
Further reading (9)