Index

Advanced Mapping Techniques

Java Streams

10.1 FlatMapping with flatMap()

The flatMap() operation is a powerful tool in the Java Streams API used to flatten nested structures and combine multiple streams into a single continuous stream. While map() transforms each element into exactly one output element (one-to-one mapping), flatMap() transforms each element into zero or more elements (one-to-many mapping) and then flattens the results into a single stream.

How flatMap() Differs from map()

This is useful when you deal with nested collections or want to transform each element into multiple elements.

Example 1: Flattening a List of Lists

Suppose you have a list of lists and want to process all inner elements as a single stream.

import java.util.List;

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

        System.out.println("Before flatMap (listOfLists): " + listOfLists);

        List<String> flattened = listOfLists.stream()
                                           .flatMap(List::stream)  // flatten inner lists
                                           .toList();

        System.out.println("After flatMap (flattened): " + flattened);
    }
}

Output:

Before flatMap (listOfLists): [[apple, banana], [cherry, date], [elderberry]]
After flatMap (flattened): [apple, banana, cherry, date, elderberry]

Example 2: Splitting Strings into Words

Transform a stream of sentences into a stream of words by splitting each sentence and flattening the resulting arrays.

import java.util.List;
import java.util.Arrays;

public class FlatMapExample2 {
    public static void main(String[] args) {
        List<String> sentences = List.of(
            "Java streams are powerful",
            "flatMap is very useful",
            "functional programming rocks"
        );

        System.out.println("Before flatMap (sentences): " + sentences);

        List<String> words = sentences.stream()
                                      .flatMap(sentence -> Arrays.stream(sentence.split(" ")))
                                      .toList();

        System.out.println("After flatMap (words): " + words);
    }
}

Output:

Before flatMap (sentences): [Java streams are powerful, flatMap is very useful, functional programming rocks]
After flatMap (words): [Java, streams, are, powerful, flatMap, is, very, useful, functional, programming, rocks]

Example 3: Processing Nested Objects

Assume you have a list of User objects, each with a list of email addresses, and you want to get all emails in a single stream.

import java.util.List;

public class FlatMapExample3 {
    static class User {
        String name;
        List<String> emails;
        User(String name, List<String> emails) {
            this.name = name;
            this.emails = emails;
        }
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Alice", List.of("alice@example.com", "alice.work@example.com")),
            new User("Bob", List.of("bob@example.com")),
            new User("Carol", List.of())
        );

        System.out.println("Before flatMap (users): " + users.size() + " users");

        List<String> allEmails = users.stream()
                                      .flatMap(user -> user.emails.stream())
                                      .toList();

        System.out.println("After flatMap (allEmails): " + allEmails);
    }
}

Output:

Before flatMap (users): 3 users
After flatMap (allEmails): [alice@example.com, alice.work@example.com, bob@example.com]

Summary

By mastering flatMap(), you can write concise, expressive code that handles complex data structures with ease.

Index

10.2 Mapping to Multiple Collections

In some situations, you want to process a stream once but collect the results into multiple collections simultaneouslyβ€”for example, splitting data into categories or producing different aggregations from the same data source.

Java 12 introduced Collectors.teeing(), a powerful utility that allows you to run two collectors in parallel on the same stream and then combine their results. Before Java 12, this kind of logic required multiple stream passes or manual splitting.

Using Collectors.teeing()

teeing() takes three arguments:

Example 1: Splitting Even and Odd Numbers into Separate Lists

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class TeeingExample1 {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        var result = numbers.stream()
            .collect(Collectors.teeing(
                // Collector for even numbers
                Collectors.filtering(n -> n % 2 == 0, Collectors.toList()),
                // Collector for odd numbers
                Collectors.filtering(n -> n % 2 != 0, Collectors.toList()),
                // Merge into a Map
                (evens, odds) -> Map.of("evens", evens, "odds", odds)
            ));

        System.out.println("Evens: " + result.get("evens"));
        System.out.println("Odds: " + result.get("odds"));
    }
}

Output:

Evens: [2, 4, 6, 8, 10]
Odds: [1, 3, 5, 7, 9]

Example 2: Computing Multiple Summaries (Count and Sum) from the Same Stream

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class TeeingExample2 {
    public static void main(String[] args) {
        List<Integer> numbers = List.of(10, 20, 30, 40, 50);

        var summary = numbers.stream()
            .collect(Collectors.teeing(
                Collectors.counting(),
                Collectors.summingInt(Integer::intValue),
                (count, sum) -> Map.of("count", count, "sum", sum)
            ));

        System.out.println("Count: " + summary.get("count"));
        System.out.println("Sum: " + summary.get("sum"));
    }
}

Output:

Count: 5
Sum: 150

Alternative: Splitting Streams Without teeing() (Pre-Java 12)

If Collectors.teeing() is unavailable, you can process the stream twice or use partitioningBy() for boolean splits:

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

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

        Map<Boolean, List<Integer>> partitioned = numbers.stream()
            .collect(Collectors.partitioningBy(n -> n % 2 == 0));

        System.out.println("Evens: " + partitioned.get(true));
        System.out.println("Odds: " + partitioned.get(false));
    }
}

Summary

These techniques empower you to write expressive, high-performance data processing pipelines with Java Streams.

Index

10.3 Complex Transformations

Complex transformations in streams go beyond simple one-to-one mappings. They often involve conditional logic, extracting multiple fields, and nesting maps to reshape data into new forms like DTOs (Data Transfer Objects) or JSON-ready structures. By composing smaller transformations, you can build modular and readable pipelines for sophisticated data processing.

Key Patterns in Complex Transformations

Example 1: Summarizing Transactions by Customer and Category

Given a list of transactions, group them by customer, then by category, and calculate total amounts per group.

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class ComplexTransformationExample1 {

    static class Transaction {
        String customer;
        String category;
        double amount;

        Transaction(String customer, String category, double amount) {
            this.customer = customer;
            this.category = category;
            this.amount = amount;
        }
    }

    public static void main(String[] args) {
        List<Transaction> transactions = List.of(
            new Transaction("Alice", "Books", 120.0),
            new Transaction("Alice", "Electronics", 200.0),
            new Transaction("Bob", "Books", 80.0),
            new Transaction("Bob", "Books", 50.0),
            new Transaction("Alice", "Books", 30.0)
        );

        Map<String, Map<String, Double>> report = transactions.stream()
            .collect(Collectors.groupingBy(
                t -> t.customer,
                Collectors.groupingBy(
                    t -> t.category,
                    Collectors.summingDouble(t -> t.amount)
                )
            ));

        System.out.println("Customer spending report:");
        System.out.println(report);
    }
}

Output:

Customer spending report:
{Alice={Books=150.0, Electronics=200.0}, Bob={Books=130.0}}

This nested grouping and summing provides a clear multi-level summary of spending per customer and category.

Example 2: Mapping Entities to JSON-Ready Nested DTOs with Conditional Fields

Suppose you have Person objects with optional addresses and phone numbers. You want to convert them into nested DTOs suitable for JSON serialization, skipping empty data.

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class ComplexTransformationExample2 {

    static class Person {
        String name;
        String email;
        List<String> phones;
        Address address;

        Person(String name, String email, List<String> phones, Address address) {
            this.name = name;
            this.email = email;
            this.phones = phones;
            this.address = address;
        }
    }

    static class Address {
        String street;
        String city;

        Address(String street, String city) {
            this.street = street;
            this.city = city;
        }
    }

    static class PersonDTO {
        String name;
        String email;
        List<String> phones;
        Map<String, String> address;

        PersonDTO(String name, String email, List<String> phones, Map<String, String> address) {
            this.name = name;
            this.email = email;
            this.phones = phones;
            this.address = address;
        }

        @Override
        public String toString() {
            return "PersonDTO{" +
                   "name='" + name + '\'' +
                   ", email='" + email + '\'' +
                   ", phones=" + phones +
                   ", address=" + address +
                   '}';
        }
    }

    public static void main(String[] args) {
        List<Person> people = List.of(
            new Person("John Doe", "john@example.com", List.of("123-456-7890"), new Address("123 Elm St", "Springfield")),
            new Person("Jane Smith", "jane@example.com", List.of(), null)
        );

        List<PersonDTO> dtos = people.stream()
            .map(p -> new PersonDTO(
                p.name,
                p.email,
                p.phones.isEmpty() ? null : p.phones,
                p.address == null ? null : Map.of(
                    "street", p.address.street,
                    "city", p.address.city
                )
            ))
            .collect(Collectors.toList());

        dtos.forEach(System.out::println);
    }
}

Output:

PersonDTO{name='John Doe', email='john@example.com', phones=[123-456-7890], address={street=123 Elm St, city=Springfield}}
PersonDTO{name='Jane Smith', email='jane@example.com', phones=null, address=null}

This example shows conditional mapping (skipping empty phones and addresses) and nested object construction using maps, suitable for JSON serialization frameworks.

Summary

Complex transformations often combine:

By modularizing these transformations and composing smaller functions, you create maintainable and expressive stream pipelines that fit real-world data processing needs.

Index