Index

Integrating Streams with Functional Programming

Java Streams

20.1 Using Streams with Functional Interfaces

Java Streams are built on the foundation of functional interfaces—special interfaces with a single abstract method—that enable concise and declarative data processing pipelines. The most commonly used functional interfaces in streams are:

These interfaces empower streams to express complex transformations, filters, and side effects with minimal boilerplate.

How Functional Interfaces Enable Declarative Logic in Streams

Streams leverage these functional interfaces to build pipelines where each step declares what should be done rather than how. For example:

Using lambdas or method references makes this code clean, readable, and highly expressive.

Example 1: Filtering with Predicate

import java.util.List;

public class PredicateExample {
    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Bob", "Charlie", "David");

        // Filter names that start with 'A' using a lambda Predicate
        names.stream()
            .filter(name -> name.startsWith("A"))
            .forEach(System.out::println);
    }
}

Output:

Alice

Here, the lambda name -> name.startsWith("A") implements Predicate<String> to test each element.

Example 2: Mapping with Function

import java.util.List;

public class FunctionExample {
    public static void main(String[] args) {
        List<String> words = List.of("apple", "banana", "cherry");

        // Convert each word to uppercase using a method reference Function
        words.stream()
            .map(String::toUpperCase)
            .forEach(System.out::println);
    }
}

Output:

APPLE
BANANA
CHERRY

String::toUpperCase is a concise method reference that matches Function<String, String>.

Example 3: Consuming with Consumer

import java.util.List;

public class ConsumerExample {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4);

        // Print each number with a prefix using a custom Consumer lambda
        numbers.stream()
            .forEach(n -> System.out.println("Number: " + n));
    }
}

Output:

Number: 1
Number: 2
Number: 3
Number: 4

The lambda n -> System.out.println("Number: " + n) defines a Consumer<Integer> for side effects.

Summary

By combining functional interfaces with streams:

This synergy between Streams and functional interfaces is key to mastering modern Java data processing patterns.

Index

20.2 Composing Functions and Stream Pipelines

Function composition is a powerful technique that enhances the clarity, reusability, and modularity of stream pipelines. By combining smaller functions into larger ones, you can build complex transformations or filters in a clean, declarative manner. Java’s functional interfaces like Function and Predicate provide built-in methods to facilitate this composition:

Using these methods allows you to write reusable, composable logic blocks that can be passed directly to stream operations like map() or filter().

How Function Composition Works

This distinction is important when chaining multiple transformations, enabling flexible ordering.

Example 1: Chained Transformations with Function.andThen()

Suppose you want to process a list of names by trimming whitespace and then converting to uppercase. Instead of writing a single lambda, compose two reusable functions:

import java.util.List;
import java.util.function.Function;

public class FunctionCompositionExample {
    public static void main(String[] args) {
        List<String> names = List.of("  Alice ", " Bob", "Charlie  ");

        Function<String, String> trim = String::trim;
        Function<String, String> toUpperCase = String::toUpperCase;

        // Compose functions: first trim, then convert to uppercase
        Function<String, String> trimAndUpperCase = trim.andThen(toUpperCase);

        names.stream()
             .map(trimAndUpperCase)
             .forEach(System.out::println);
    }
}

Output:

ALICE
BOB
CHARLIE

By composing trim and toUpperCase, the code becomes modular and easy to maintain.

Example 2: Complex Filtering with Predicate.and() and Predicate.or()

Imagine filtering a list of integers to include only those that are positive and even, or greater than 50:

import java.util.List;
import java.util.function.Predicate;

public class PredicateCompositionExample {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(10, 25, 42, 55, 60, -4, 0);

        Predicate<Integer> isPositive = n -> n > 0;
        Predicate<Integer> isEven = n -> n % 2 == 0;
        Predicate<Integer> isGreaterThan50 = n -> n > 50;

        // Compose predicates: (positive AND even) OR greater than 50
        Predicate<Integer> complexCondition = isPositive.and(isEven).or(isGreaterThan50);

        numbers.stream()
               .filter(complexCondition)
               .forEach(System.out::println);
    }
}

Output:

10
42
55
60

This example cleanly expresses a complex conditional filter by composing simple predicates.

Benefits of Composing Functions in Stream Pipelines

Summary

Function composition using Function.andThen(), compose(), and Predicate.and()/or() empowers you to build elegant, reusable logic blocks for stream pipelines. This approach leads to cleaner, more declarative code when performing chained transformations or complex filtering — essential for writing maintainable and expressive Java stream-based data processing.

Index

20.3 Currying and Partial Application Concepts

Currying and partial application are foundational concepts in functional programming that enable building flexible, reusable functions by breaking down functions with multiple arguments into chains of single-argument functions. These techniques can greatly simplify and modularize logic when working with Java Streams.

What Is Currying?

Currying transforms a function that takes multiple arguments into a sequence of functions each taking a single argument. For example, a function of two arguments (A, B) -> R becomes A -> (B -> R) — a function that returns another function.

In Java, currying is often implemented with lambdas that return other lambdas.

What Is Partial Application?

Partial application fixes a few arguments of a multi-argument function, producing a new function of fewer arguments. For example, partially applying the first argument of a two-argument function f(A, B) yields a single-argument function g(B).

Why Does This Matter for Streams?

Currying and partial application help build reusable and parameterized stream operations, such as:

This leads to concise, composable pipelines without repetitive boilerplate.

Example 1: Curried Predicate Generator for Filtering

Suppose you want to create a filter predicate to check if strings have length greater than a dynamic threshold. Currying helps create a reusable predicate factory:

import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;

public class CurryingExample {
    // Curried function: int -> Predicate<String>
    static Function<Integer, Predicate<String>> lengthGreaterThan =
        length -> str -> str.length() > length;

    public static void main(String[] args) {
        List<String> words = List.of("apple", "pear", "banana", "kiwi", "pineapple");

        // Partially apply length = 4
        Predicate<String> longerThan4 = lengthGreaterThan.apply(4);

        words.stream()
             .filter(longerThan4)
             .forEach(System.out::println);
    }
}

Output:

apple
banana
pineapple

Here, lengthGreaterThan is a curried function that produces a predicate configured by a threshold. This keeps filtering logic reusable and clean.

Example 2: Partial Application for Custom Mapping

Imagine mapping integers to strings with a customizable prefix. You can partially apply the prefix to create reusable mappers:

import java.util.List;
import java.util.function.Function;

public class PartialApplicationExample {
    // Function that takes prefix and returns function from Integer to String
    static Function<String, Function<Integer, String>> prefixer =
        prefix -> number -> prefix + number;

    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5);

        // Partially apply prefix "Item-"
        Function<Integer, String> itemNamer = prefixer.apply("Item-");

        numbers.stream()
               .map(itemNamer)
               .forEach(System.out::println);
    }
}

Output:

Item-1
Item-2
Item-3
Item-4
Item-5

This pattern can generate various mapping functions dynamically based on context.

Summary

Currying and partial application bring powerful abstraction to stream processing by enabling you to build parameterized, reusable functions that cleanly integrate with map(), filter(), and other stream operations. By structuring lambdas as chains of single-argument functions, you achieve more expressive and maintainable pipelines that adapt flexibly to different use cases.

Index