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.
flatMap()
Differs from map()
map()
takes a function that returns a single element and produces a stream of those elements.flatMap()
takes a function that returns a stream or collection for each element, then concatenates (flattens) all those inner streams into one flat stream.This is useful when you deal with nested collections or want to transform each element into multiple elements.
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]
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]
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]
map()
for one-to-one transformations.flatMap()
when each element maps to multiple elements or a nested stream, and you want to flatten them into a single stream.By mastering flatMap()
, you can write concise, expressive code that handles complex data structures with ease.
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.
Collectors.teeing()
teeing()
takes three arguments:
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]
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
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));
}
}
Collectors.teeing()
to collect stream data into multiple collections or summaries in one pass.partitioningBy()
is a useful shortcut.These techniques empower you to write expressive, high-performance data processing pipelines with Java Streams.
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.
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.
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.
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.