Java Fundamentals Tutorial 0/33 lessons ~6 min read Lesson 18

    Functional Programming

    Before Java 8, passing behavior meant anonymous inner classes — 15 lines of boilerplate to filter a list of wire transfers.

    Course progress0%
    Focus
    21 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Before Java 8, passing behavior meant anonymous inner classes — 15 lines of boilerplate to filter a list of wire transfers. Today, every Spring Security filter, CompletableFuture callback, and Stream pipeline uses functional programming primitives: lambdas, method references, functional interfaces, and Optional.

    This lesson teaches FP from an enterprise banking perspective — not academic category theory, but how payment validators, fraud rules, and ledger mappers express behavior as first-class values. You will learn lambda syntax and capture semantics, the four method reference forms, built-in functional interfaces (Predicate, Function, Consumer, Supplier), and how Optional replaces null checks without creating new categories of bugs.

    Functional style is the glue between Streams, CompletableFuture, and modern Spring APIs — staff engineers read lambdas daily in code review and must spot capture bugs, serialization pitfalls, and Optional misuse that cause production NPEs.

    Business problem

    Without functional idioms, enterprise Java becomes unmaintainable:

    • Callback hell: Anonymous classes for async payment confirmation — nested 6 deep, untestable.
    • NullPointerException in prod: account.getAddress().getCountry() on nullable KYC fields — $2M failed SWIFT batch.
    • Untestable predicates: Business rules buried in 200-line methods — can't unit test "is high-risk merchant" in isolation.
    • API pollution: Overloaded methods for every behavior variant instead of passing Predicate<Payment>.

    Why this topic exists

    Java added functional features to stay competitive while preserving backward compatibility — no new keywords except lambda syntax sugar.

    • Behavior as data: Pass validation rules to framework — Spring @PreAuthorize, JUnit dynamic tests.
    • Composition: Predicate.and/or/negate, Function.compose/andThen — build rules from smaller rules.
    • Interoperability: Same functional interface works with lambdas, method refs, and anonymous classes.
    • Optional: Explicit absence in type system — forces caller to handle missing beneficiary account.

    Core concepts

    Four pillars of Java functional programming:

    • Lambdas: (payment) -> payment.amountCents() > limit — concise implementation of single-method interface.
    • Method references: Payment::isAuthorized — shorthand when lambda delegates to existing method.
    • Functional interfaces: @FunctionalInterface with one abstract method — Predicate, Function, Consumer, Supplier, custom validators.
    • Optional: Optional<Account> — container for present/absent; map/filter/orElseThrow for safe chains.

    Internal architecture

    Functional interface taxonomy:

    text
    Core java.util.function (payment domain examples):
    Predicate<Payment> T → boolean p -> p.isAuthorized()
    Function<Payment,Dto> T → R p -> toDto(p)
    Consumer<Payment> T → void p -> audit.log(p)
    Supplier<Payment> () → T () -> factory.create()
    Method reference forms:
    Payment::isAuthorized — instance method on argument (bound)
    Payment::normalize — static method
    this::validate — instance method on enclosing object
    Account::getBalance — instance method on specific type (unbound)
    Optional chain (beneficiary lookup):
    Optional.ofNullable(beneficiaryId)
    .flatMap(repo::findById)
    .filter(Account::isActive)
    .map(Account::swiftCode)
    .orElseThrow(() -> new BeneficiaryNotFoundException(id));
    Lambda capture:
    effectively final local variables only — cannot reassign amountCents
    instance fields mutable — avoid mutating in parallel streams

    Four concepts — each appears in every modern payment service codebase.

    1. Lambdas

    Behavior passed to filter
    List<Payment>
    source
    lambda
    p -> p.isAuthorized()
    Predicate
    functional IF
    filtered
    result
    Lambda implements functional interface SAM without named class.
    • Syntax: (Payment p) -> p.amountCents() > 10_000 — types inferred when unambiguous.
    • SAM rule: Target type must be functional interface with exactly one abstract method — compiler generates invokedynamic bytecode.
    • Effectively final: Captured locals cannot be reassigned — use array holder or AtomicLong hack only in tests, not prod.
    • Block body: p -> { log(p); return p.isAuthorized(); } — explicit return required in blocks.

    2. Method References

    Shorthand for delegate lambdas
    p -> p.isAuthorized()
    lambda
    equivalent
    Payment::isAuthorized
    method ref
    cleaner
    review win
    Use method references when lambda only calls one method.
    • Instance on arg: Payment::merchantId — equivalent to p -> p.merchantId().
    • Static: Payment::normalizeIdp -> Payment.normalizeId(p).
    • Constructor: PaymentDto::newPaymentDto::new for mapping constructors.
    • Bound instance: validator::checkLimit — captures specific validator bean in Spring service.

    3. Functional Interfaces

    Compose fraud rules with Predicate
    isAuthorized
    rule 1
    and
    compose
    isHighRisk
    rule 2
    negate
    invert
    Standard interfaces enable rule composition without inheritance.
    • Predicate: Predicate<Payment> ok = Payment::isAuthorized; ok.and(p -> p.amountCents() < limit);
    • Function: Function<Payment, WireInstruction> — map domain to outbound message.
    • Custom @FunctionalInterface: PaymentValidator { ValidationResult validate(Payment p); } — domain-specific SAM.
    • Primitive specializations: LongPredicate, ToLongFunction — avoid boxing in amount checks.

    4. Optional

    Safe beneficiary lookup chain
    findById
    may be empty
    filter active
    Optional
    map swift
    transform
    orElseThrow
    terminal
    Optional forces explicit handling of missing account.
    • Never Optional field: Use on return types and chain locals — not class Account { Optional<String> nickname; }.
    • flatMap: When next step returns Optional — opt.flatMap(repo::findLinkedAccount) avoids nested Optional.
    • orElse vs orElseGet: orElseGet(Supplier) lazy — expensive default only computed if empty.
    • ifPresentOrElse: Java 9+ — consume value or run empty action without throwing.

    Code walkthrough

    Functional payment validation — lambdas, method refs, Predicate composition, Optional:

    • Method reference: Payment::authorized implements PaymentValidator SAM.
    • Default method composition: Custom and() mirrors Predicate.and — reusable rule building.
    • Function: Maps Payment to wire reference string — pure transformation, no side effects.
    • Optional chain: flatMap for nullable map lookup — orElse for safe default SWIFT code.
    • ifPresentOrElse: Handle present/absent without get() — avoids NoSuchElementException.
    java
    import java.util.*;
    import java.util.function.*;
    record Payment(String id, long amountCents, boolean authorized, String merchantId) {}
    @FunctionalInterface
    interface PaymentValidator {
    boolean test(Payment p);
    default PaymentValidator and(PaymentValidator other) {
    return p -> test(p) && other.test(p);
    }
    }
    class FunctionalPaymentDemo {
    static final long WIRE_LIMIT_CENTS = 1_000_000L;
    static PaymentValidator authorized() {
    return Payment::authorized; // method reference
    }
    static PaymentValidator underWireLimit() {
    return p -> p.amountCents() <= WIRE_LIMIT_CENTS; // lambda
    }
    public static void main(String[] args) {
    List<Payment> batch = List.of(
    new Payment("P1", 500_000L, true, "M1"),
    new Payment("P2", 2_000_000L, true, "M2"),
    new Payment("P3", 100_000L, false, "M3")
    );
    PaymentValidator validator = authorized().and(underWireLimit());
    List<Payment> approved = batch.stream()
    .filter(validator::test)
    .toList();
    System.out.println("Approved: " + approved.size());
    Function<Payment, String> toWireRef = p ->
    "WIRE-" + p.id() + "-" + p.merchantId();
    approved.forEach(p -> System.out.println(toWireRef.apply(p)));
    // Optional — beneficiary lookup simulation
    Map<String, String> swiftByMerchant = Map.of("M1", "ACMEUS33");
    Optional<String> swift = Optional.ofNullable(batch.get(0).merchantId())
    .flatMap(id -> Optional.ofNullable(swiftByMerchant.get(id)));
    System.out.println("SWIFT: " + swift.orElse("UNKNOWN"));
    Optional<Payment> overLimit = batch.stream()
    .filter(p -> p.amountCents() > WIRE_LIMIT_CENTS)
    .findFirst();
    overLimit.ifPresentOrElse(
    p -> System.out.println("Review: " + p.id()),
    () -> System.out.println("No over-limit payments")
    );
    }
    }

    Production example

    Spring service — injectable predicates and Optional returns:

    • Composable validators: Spring injects List of PaymentValidator — reduce with and().
    • Optional from repository: Spring Data returns Optional — chain filter/orElseThrow.
    • Method refs in streams: PaymentValidator::and as reduce accumulator.
    • No null returns: orElseThrow documents failure mode — maps to HTTP 404/422.
    java
    @Service
    public class WireTransferService {
    private final BeneficiaryRepository beneficiaries;
    private final List<PaymentValidator> validators;
    public WireTransferService(BeneficiaryRepository beneficiaries,
    List<PaymentValidator> validators) {
    this.beneficiaries = beneficiaries;
    this.validators = validators;
    }
    public WireInstruction initiate(WireCommand cmd) {
    Payment payment = cmd.toPayment();
    PaymentValidator combined = validators.stream()
    .reduce(PaymentValidator::and)
    .orElse(p -> true);
    if (!combined.test(payment)) {
    throw new ValidationException("Payment failed composite rules");
    }
    Beneficiary ben = beneficiaries.findById(cmd.beneficiaryId())
    .filter(Beneficiary::isActive)
    .orElseThrow(() -> new BeneficiaryNotFoundException(cmd.beneficiaryId()));
    return WireInstruction.from(payment, ben);
    }
    }
    // Custom validator bean
    @Component
    class SanctionsValidator implements PaymentValidator {
    @Override public boolean test(Payment p) {
    return !sanctionsList.contains(p.merchantId());
    }
    }

    Enterprise case study

    Null beneficiary SWIFT incident: A corporate banking API returned raw Account from JPA — nullable address field. Service called account.getAddress().getCountry() without null check; 12,000 international wires failed with NPE over a weekend when address migration left gaps. Fix: repository methods return Optional<Account>; service uses flatMap(Account::getAddress).map(Address::getCountry).orElse(DEFAULT_COUNTRY) with audit log on empty. Secondary: replaced anonymous Runnable audit tasks with lambdas capturing payment ID (effectively final) — fixed closure bug where loop variable was shared incorrectly in pre-Java-8 style port.

    • Symptom: Spike in 500 errors on /wire/international — all NPE at same line.
    • Root cause: Nullable FK not modeled in API — imperative null checks skipped in refactor.
    • Fix: Optional return types + static analysis (NullAway) on payment module.
    • Lesson: Optional documents contract; don't use as field type or serialization payload.

    Performance considerations

    Functional code performance:

    • Lambda allocation: First capture may allocate; JIT inlines hot lambdas — rarely bottleneck vs DB I/O.
    • Method reference vs lambda: Identical performance after JIT — choose readability.
    • Primitive functional interfaces: LongPredicate for amount rules — avoids autoboxing in tight loops.
    • Optional overhead: Single object allocation — negligible; don't use in innermost nanosecond loops unnecessarily.
    • Stream + lambda: Combined abstraction cost — still dominated by business logic and I/O in payment services.

    Security considerations

    Functional patterns and security:

    • Predicate injection: Don't expose arbitrary Predicate from user input — rule injection in dynamic filters.
    • Lambda serialization: Serializable lambdas capture enclosing state — may leak credentials in captured fields.
    • Optional.get(): Throws if empty — use orElseThrow with domain exception, never bare get() in prod.
    • Method ref to privileged method: Passing this::internalValidate to untrusted callback — review exposure.

    Scalability considerations

    Scaling functional pipelines:

    • Stateless lambdas: Required for parallel streams and reactive — no mutating captured collections.
    • Validator registry: Compose rules at startup — O(rules) per payment, cache combined predicate if static.
    • Async composition: CompletableFuture.thenApplyAsync with Function — thread pool sizing separate concern.
    • Optional in hot Kafka path: Millions msgs/sec — prefer primitive sentinels or sealed types over Optional boxing if profiled hot.

    Production challenges

    Real functional Java failures:

    • Effectively final violation: Compile error when reassigning captured variable — junior dev uses single-element array workaround (anti-pattern).
    • Optional in JSON API: Jackson serializes Optional wrapper weirdly — use DTO fields, not Optional on REST models.
    • Method ref wrong overload: Ambiguous Payment::process when multiple process methods exist.
    • Mutable field capture: Lambda mutates instance field in parallel stream — race on validator counters.
    • orElse expensive call: orElse(computeDefault()) always runs — use orElseGet.

    Common mistakes

    • Using Optional as field type or method parameter — intended for return types only.
    • Calling Optional.get() without isPresent — use orElseThrow with meaningful exception.
    • orElse(expensive()) instead of orElseGet(() -> expensive()) — default computed even when present.
    • Non-static inner class vs lambda — lambda doesn't hold implicit outer this unless captured.
    • Serializable lambda capturing Spring @Service — serialVersionUID and secret field leaks.

    Debugging guide

    Debug lambdas and Optional in production:

    • Stack traces: Lambda methods show as ClassName$Lambda$123/0x... — enable -parameters flag for arg names.
    • peek in stream: Temporary Consumer lambda for tracing — remove before merge.
    • Optional breakpoint: Conditional break on optional.isEmpty() at orElseThrow site.
    • Unit test predicates: Isolate PaymentValidator beans — table-driven tests for rule matrix.
    • NullAway/SpotBugs: Static analysis catches nullable dereference better than Optional alone.
    bash
    # Enable parameter names for readable lambda stacks (Maven)
    <compilerArgs>
    <arg>-parameters</arg>
    </compilerArgs>
    # JUnit — test composed validator
    @Test
    void rejectsUnauthorizedOverLimit() {
    PaymentValidator v = authorized().and(underWireLimit());
    assertFalse(v.test(new Payment("X", 2_000_000L, false, "M")));
    }

    Best practices

    • Use method references when lambda only delegates — improves readability in stream pipelines.
    • Return Optional from find/query methods; use orElseThrow for required entities like Beneficiary.
    • Compose small Predicate/Function units — test each rule independently.
    • Prefer orElseGet over orElse for expensive defaults.
    • Mark custom SAM interfaces @FunctionalInterface — compiler enforces single abstract method.
    • Keep lambdas short — extract to named private methods when block exceeds 3 lines.
    • Use primitive functional interfaces for amount/limit checks in hot paths.

    Anti-patterns

    • Optional.of(null) — throws NPE; use ofNullable.
    • Optional as JSON field or JPA entity attribute.
    • get() without check — NoSuchElementException in prod.
    • Mutable collection captured and modified in parallel forEach.
    • Over-engineering: Optional<Optional<T>> — use flatMap.

    Staff engineer notes

    • Lambdas didn't remove null — Optional and discipline did; many codebases still NPE with Optional.get().
    • Method references in stream pipelines are the readability bar — p -> p.foo() when ::foo exists fails review.
    • Custom @FunctionalInterface validators beat boolean flags — composable and testable.
    • Spring's Optional wrapper on Page/findById is the model — repository never returns raw null.
    • In code review: flag any orElse with method call — should almost always be orElseGet.

    Interview questions

    Interview preparation

    15 questions grouped by difficulty — expand any card for a model answer and follow-up probe.

    Beginner

    5
    1. 1What is a lambda expression in Java?
      Beginner

      Model answer

      Anonymous function implementing single abstract method of functional interface.

      Syntax (args) -> body.

      Compiler uses invokedynamic; can capture effectively final locals and instance fields.

      Follow-up probe

      Effectively final?

    2. 2Explain the four method reference types.
      Beginner

      Model answer

      Static: ClassName::staticMethod.

      foo()).

      Bound instance: instance::method.

      Constructor: ClassName::new.

      Follow-up probe

      When constructor ref?

    3. 3What is a functional interface?
      Beginner

      Model answer

      Interface with one abstract method (SAM).

      @FunctionalInterface annotation optional but recommended.

      Can have default and static methods.

      Lambda or method ref provides implementation.

      Follow-up probe

      Is Runnable functional?

    4. 4Difference between Predicate and Function?
      Beginner

      Model answer

      Predicate takes T returns boolean — filter/test. Function takes T returns R — map/transform. Both support composition (and/compose).

      Follow-up probe

      BiFunction?

    5. 5What is Optional and when use it?
      Beginner

      Model answer

      • Container for value or empty. Use as return type when absence is valid
      • findById. Chain map/filter/flatMap. orElseThrow for required values. Don't use as field or parameter.

      Follow-up probe

      Optional.of vs ofNullable?

    Intermediate

    5
    1. 6orElse vs orElseGet?
      Intermediate

      Model answer

      • orElse(value) evaluates argument always
      • even if Optional present. orElseGet(Supplier) lazy
      • supplier only on empty. Use orElseGet for expensive defaults or DB lookups.

      Follow-up probe

      orElseThrow supplier?

    2. 7What does flatMap do on Optional?
      Intermediate

      Model answer

      If present, applies function returning Optional — flattens Optional> to Optional. Use when next step may fail/absent — repo.findLinked(id).

      Follow-up probe

      Optional stream flatMap?

    3. 8Can lambdas access local variables?
      Intermediate

      Model answer

      • Yes if effectively final
      • not reassigned after capture. Instance/static fields accessible. this refers to enclosing instance for instance methods.

      Follow-up probe

      Why effectively final?

    4. 9Compare anonymous class vs lambda.
      Intermediate

      Model answer

      Lambda only for functional interfaces; shorter syntax.

      Anonymous class can implement multiple methods, have state, named this.

      class file.

      Follow-up probe

      Serialization difference?

    5. 10How compose validation rules functionally?
      Intermediate

      Model answer

      Predicate.and/or/negate or custom default and on @FunctionalInterface. Stream of validators reduce with and. Spring inject List compose at runtime.

      Follow-up probe

      Short-circuit?

    Advanced

    5
    1. 11Performance of lambdas vs inner classes?
      Advanced

      Model answer

      • HotSpot inlines both after warmup. Lambda slight bootstrap via invokedynamic. Not bottleneck in enterprise apps
      • prefer clarity. Profile before micro-optimizing away lambdas.

      Follow-up probe

      Escape analysis?

    2. 12Design PaymentValidator as functional interface.
      Advanced

      Model answer

      @FunctionalInterface with boolean test(Payment).

      Default and() for composition.

      Spring @Component validators injected as List.

      Service reduces to combined rule.

      Unit test each validator + integration test composite.

      Follow-up probe

      Exception in validator?

    3. 13Optional anti-patterns in API design?
      Advanced

      Model answer

      ofNullable chain when simple null check clearer for 1 step.

      Use for return types signaling absence.

      Follow-up probe

      Java 21 Optional.isEmpty?

    4. 14How Optional interacts with Stream?
      Advanced

      Model answer

      • stream.flatMap(o -> o.stream()) Java 9+
      • filter empty optionals. map on optional similar to stream map. Avoid Optional.get in stream
      • use orElseThrow or flatMap.

      Follow-up probe

      OptionalStream in Java 9?

    5. 15Migrate null checks to Optional in legacy banking code.
      Advanced

      Model answer

      Phase 1: repository find methods return Optional.

      Phase 2: service chains flatMap/map.

      Phase 3: NullAway static analysis.

      Don't mass-Optional fields.

      Document orElseThrow exceptions in API.

      Regression test nullable scenarios from production incidents.

      Follow-up probe

      Kotlin interop?

    Hands-on exercise

    Lab: Functional payment rules

    • Run playground — observe composed validator filtering batch.
    • Add new PaymentValidator for blocked merchant list — compose with and().
    • Convert one lambda to method reference and vice versa — confirm equivalent.
    • Break effectively final — observe compile error; fix properly.
    • Replace orElse with orElseGet and log when supplier runs.
    • Bonus: write JUnit table test for 4 payment scenarios against combined validator.

    JavaFunctional Programming

    Starter Templates
    OutputRemote JVM (Piston · Java 15)
    Press Run to execute. Real javac + JVM via remote API. Falls back to simulator on network failure.

    Architecture trade-offs

    • Lambda vs named method: Lambda inline; named method debuggable and reusable.
    • Optional vs null: Optional explicit but allocates; null zero-cost if disciplined.
    • Custom SAM vs Predicate: Domain validator clearer; Predicate standard compose API.
    • Method ref vs lambda: Same performance; ref wins readability when direct delegate.

    Summary

    Functional programming in Java is practical machinery for payment validation, stream pipelines, and async callbacks — lambdas, method references, functional interfaces, and Optional. Compose small rules, return Optional from lookups, and avoid known anti-patterns. Next: Records & Sealed Classes — immutable data carriers and exhaustive domain modeling with pattern matching.

    Key takeaways

    • Lambdas and method references implement functional interfaces — behavior as first-class values.
    • Predicate, Function, Consumer, Supplier cover most payment pipeline needs — compose with and/compose.
    • Optional on return types replaces null checks — use flatMap chains, orElseGet, orElseThrow.
    • Keep lambdas pure and short — extract validators as testable @FunctionalInterface beans.
    • Never Optional.get() in production — never Optional on DTO fields or JPA entities.
    Ready to mark this lesson complete?Track your journey across the entire course.