Index

Appendix

Java Functional Programming

17.1 Common Functional Interfaces and Usage Examples

Java’s java.util.function package provides a rich set of functional interfaces—interfaces with a single abstract method—that serve as building blocks for functional programming. Understanding these interfaces helps write concise, reusable, and expressive lambda expressions and method references.

Below are the most frequently used functional interfaces, their method signatures, and typical use cases:

Function<T, R>

Function<String, Integer> stringLength = s -> s.length();
int len = stringLength.apply("hello");  // returns 5

Tip: Use Function when you need to convert or transform one type into another.

PredicateT

Predicate<String> isEmpty = String::isEmpty;
boolean result = isEmpty.test("");  // returns true

Tip: Use Predicate for conditions, filters, and validations.

Consumer<T>

Consumer<String> print = System.out::println;
print.accept("Hello World");  // Prints: Hello World

Tip: Use Consumer when you want to perform an action with an input but don't need to return anything.

Supplier<T>

Supplier<Double> randomSupplier = Math::random;
double randomValue = randomSupplier.get();

Tip: Use Supplier to defer execution or provide default/lazy values.

UnaryOperator<T>

UnaryOperator<String> toUpperCase = String::toUpperCase;
String result = toUpperCase.apply("java");  // returns "JAVA"

Tip: Use UnaryOperator when transforming an object to another instance of the same type.

BiFunction<T, U, R>

BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
int sum = add.apply(5, 7);  // returns 12

Tip: Use BiFunction when you need a function with two inputs and one output.

Choosing the Right Interface

Summary

Java’s built-in functional interfaces form the foundation for clean, expressive functional code. By selecting the right interface based on your operation’s input and output requirements, you simplify code and enable easy composition of behavior with lambdas and method references.

Mastering these interfaces unlocks the full power of Java’s functional programming capabilities.

Index

17.2 Writing Efficient Functional Java Code

Functional programming in Java offers expressive and concise ways to write code, but writing efficient functional code requires attention to performance details. Here are key considerations and best practices to help you write clean and performant functional Java code.

Minimize Unnecessary Object Creation

Lambdas and streams can generate many temporary objects (e.g., boxed primitives, intermediate collections). Excessive object creation increases GC pressure and slows down your application.

// Prefer IntStream over Stream<Integer> to avoid boxing
int sum = IntStream.range(1, 1000)
                   .filter(i -> i % 2 == 0)
                   .sum();

Avoid Expensive Intermediate Operations

Intermediate stream operations are lazy but can be costly if applied incorrectly.

// Stop processing after finding the first even number
OptionalInt firstEven = IntStream.range(1, 1_000_000)
                                 .filter(i -> i % 2 == 0)
                                 .findFirst();

Choose Between Sequential and Parallel Streams Wisely

Parallel streams can speed up CPU-bound operations but add overhead due to thread management.

List<String> data = ...;
// Use parallel stream only when justified
List<String> results = data.parallelStream()
                           .filter(s -> s.length() > 5)
                           .collect(Collectors.toList());

Leverage Method References and Avoid Capturing Variables

Method references (Class::method) are often more efficient than lambdas because they can avoid capturing variables and thus reduce object allocation.

// Efficient and clean method reference
list.stream()
    .map(String::toUpperCase)
    .forEach(System.out::println);

Keep Pipelines Simple and Readable

Overly complex pipelines with nested lambdas hurt readability and maintainability, which indirectly affect long-term performance due to bugs or poor optimizations.

Summary

Efficient functional Java code balances readability with performance. Use primitive streams to avoid boxing, leverage short-circuiting to limit processing, carefully decide when to parallelize, and prefer method references for concise, low-overhead lambdas. By combining these best practices, you write clean, maintainable, and performant functional programs. Always measure performance impacts in the context of your real application to make informed decisions.

Index

17.3 Common Pitfalls and How to Avoid Them

Adopting functional programming in Java brings many benefits but also introduces pitfalls, especially for developers new to the paradigm. Being aware of these common mistakes helps you write more robust, maintainable functional code.

Mutating State Inside Streams

Problem: Streams are designed for declarative, side-effect-free operations. Mutating external state (e.g., modifying a collection or a variable) within stream operations breaks this model, causing unpredictable behavior, especially with parallel streams.

Why it’s problematic:

How to avoid: Use pure functions inside streams. Accumulate results using collectors or return new immutable objects instead of modifying shared state.

// Bad: mutating external list inside stream
List<String> names = new ArrayList<>();
stream.forEach(s -> names.add(s.toUpperCase())); // Unsafe!

// Good: collect results immutably
List<String> names = stream
                     .map(String::toUpperCase)
                     .collect(Collectors.toList());

Improper Exception Handling in Streams

Problem: Checked exceptions cannot be thrown directly from lambdas, which leads to boilerplate or swallowing exceptions inside streams.

Why it’s problematic:

How to avoid: Wrap checked exceptions into unchecked ones or create utility methods to handle exceptions functionally.

// Example utility wrapper for checked exceptions
static <T, R> Function<T, R> wrap(CheckedFunction<T, R> func) {
    return t -> {
        try {
            return func.apply(t);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    };
}

// Usage:
stream.map(wrap(s -> someIOOperation(s)))
      .collect(Collectors.toList());

Misusing Parallel Streams

Problem: Parallel streams don’t always improve performance and can degrade it due to overhead or thread contention.

Why it’s problematic:

How to avoid:

Overcomplicating Simple Problems

Problem: Functional style can tempt developers to over-engineer solutions using streams or lambdas where simple loops or conditionals suffice.

Why it’s problematic:

How to avoid:

Summary

Avoid mutating state in streams, handle exceptions thoughtfully, use parallel streams judiciously, and resist overcomplicating code. These guidelines will help you write clear, correct, and efficient functional Java programs. Functional programming isn’t about forcing every piece of code into lambdas but about leveraging their power where they provide genuine benefits.

Index