← Назад к вопросам

Где можно использовать flatMap?

1.7 Middle🔥 231 комментариев
#Stream API и функциональное программирование

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Использование flatMap

flatMap — это мощная операция для работы с потоками и коллекциями, которая одновременно трансформирует и "разворачивает" вложенные структуры.

Что такое flatMap

flatMap = map (трансформация) + flatten (разворачивание). Если map преобразует каждый элемент, то flatMap делает тоже самое, но при этом разворачивает результаты.

Простой пример:

// map возвращает List<List<Integer>>
List<Integer> numbers = Arrays.asList(1, 2, 3);
List<List<Integer>> mapped = numbers.stream()
    .map(n -> Arrays.asList(n, n*2))
    .collect(Collectors.toList());
// Результат: [[1,2], [2,4], [3,6]]

// flatMap возвращает List<Integer> (развёрнутый)
List<Integer> flattened = numbers.stream()
    .flatMap(n -> Arrays.asList(n, n*2).stream())
    .collect(Collectors.toList());
// Результат: [1, 2, 2, 4, 3, 6]

Случай 1: Развёртывание вложенных коллекций

Задача: преобразовать список списков в единый список

public List<Integer> flattenNumbers() {
    List<List<Integer>> nestedLists = Arrays.asList(
        Arrays.asList(1, 2, 3),
        Arrays.asList(4, 5),
        Arrays.asList(6, 7, 8, 9)
    );
    
    return nestedLists.stream()
        .flatMap(List::stream)  // Развёртываем каждый подсписок
        .collect(Collectors.toList());
    // Результат: [1, 2, 3, 4, 5, 6, 7, 8, 9]
}

Случай 2: Работа с Optional

flatMap часто используется с Optional для обработки цепочек операций, которые могут вернуть пусто значение.

public class User {
    private String id;
    private String email;
    // getters...
}

public class UserRepository {
    public Optional<User> findById(String id) { /* ... */ }
}

public class EmailService {
    public Optional<String> sendEmail(String email) { /* ... */ }
}

// Без flatMap (много complexity)
public Optional<String> sendWelcomeEmail(String userId) {
    Optional<User> userOpt = userRepository.findById(userId);
    if (userOpt.isPresent()) {
        User user = userOpt.get();
        Optional<String> resultOpt = emailService.sendEmail(user.getEmail());
        if (resultOpt.isPresent()) {
            return resultOpt;
        }
    }
    return Optional.empty();
}

// С flatMap (elegantly)
public Optional<String> sendWelcomeEmail(String userId) {
    return userRepository.findById(userId)
        .flatMap(user -> emailService.sendEmail(user.getEmail()));
}

Случай 3: Трансформация с развёртыванием

Задача: найти все комментарии ко всем постам пользователя

public class Post {
    private Long id;
    private String content;
    private List<Comment> comments;
    // getters...
}

public class User {
    private String id;
    private List<Post> posts;
    // getters...
}

public List<Comment> getAllUserComments(User user) {
    return user.getPosts().stream()
        .flatMap(post -> post.getComments().stream())
        .collect(Collectors.toList());
}

// Более сложный пример: фильтрация с развёртыванием
public List<Comment> getLongCommentsFromPublishedPosts(User user) {
    return user.getPosts().stream()
        .filter(post -> post.isPublished())
        .flatMap(post -> post.getComments().stream())
        .filter(comment -> comment.getText().length() > 100)
        .collect(Collectors.toList());
}

Случай 4: Работа с потоками файлов и ресурсов

Задача: найти все слова из нескольких файлов

public List<String> getAllWordsFromFiles(List<String> filePaths) throws IOException {
    return filePaths.stream()
        .flatMap(path -> {
            try {
                return Files.lines(Paths.get(path));
            } catch (IOException e) {
                return Stream.empty();
            }
        })
        .flatMap(line -> Arrays.stream(line.split("\\s+")))  // split на слова
        .filter(word -> !word.isEmpty())
        .map(String::toLowerCase)
        .distinct()
        .collect(Collectors.toList());
}

Случай 5: Комбинирование данных из разных источников

Задача: для каждого пользователя получить его заказы и развернуть

public class Order {
    private Long id;
    private double amount;
    // getters...
}

public class OrderService {
    public List<Order> getOrdersByUserId(String userId) { /* ... */ }
}

public double getTotalOrderAmountForAllUsers(List<User> users) {
    return users.stream()
        .flatMap(user -> orderService.getOrdersByUserId(user.getId()).stream())
        .mapToDouble(Order::getAmount)
        .sum();
}

// Группировка с flatMap
public Map<String, Double> getOrderAmountPerUser(List<User> users) {
    return users.stream()
        .collect(Collectors.toMap(
            User::getId,
            user -> orderService.getOrdersByUserId(user.getId()).stream()
                .mapToDouble(Order::getAmount)
                .sum()
        ));
}

Случай 6: Работа с CompletableFuture

Задача: асинхронно получить данные и развернуть результаты

public CompletableFuture<List<String>> fetchUserDataAsync(String userId) {
    return userRepository.findByIdAsync(userId)
        .flatMap(user -> 
            emailService.getEmailHistoryAsync(user.getEmail())
                .thenApply(emails -> convertToStrings(emails))
        );
}

// Цепочка асинхронных операций
public CompletableFuture<String> processUserOrdersAsync(String userId) {
    return userRepository.findByIdAsync(userId)
        .flatMap(user -> orderService.getOrdersAsync(user.getId()))
        .flatMap(orders -> notificationService.sendBulkNotificationsAsync(orders))
        .thenApply(result -> "Processed " + result.size() + " orders");
}

Случай 7: Парсинг и трансформация данных

Задача: распарсить CSV и преобразовать в объекты

public List<User> parseUsersFromCSV(String csvContent) {
    return Arrays.stream(csvContent.split("\n"))
        .skip(1)  // Skip header
        .flatMap(line -> {
            try {
                String[] parts = line.split(",");
                User user = new User(parts[0], parts[1], parts[2]);
                return Stream.of(user);
            } catch (Exception e) {
                // Пропускаем некорректные строки
                return Stream.empty();
            }
        })
        .collect(Collectors.toList());
}

Производительность: flatMap vs alternatives

// Вариант 1: flatMap (наиболее эффективный)
List<Integer> result1 = numbers.stream()
    .flatMap(n -> generateNumbers(n).stream())
    .collect(Collectors.toList());

// Вариант 2: forEach + addAll (медленнее для потоков)
List<Integer> result2 = new ArrayList<>();
numbers.forEach(n -> result2.addAll(generateNumbers(n)));

// Вариант 3: цикл (самый понятный, но не functional)
List<Integer> result3 = new ArrayList<>();
for (Integer n : numbers) {
    result3.addAll(generateNumbers(n));
}

Когда НЕ использовать flatMap

// Плохо: flatMap для простого map
List<String> names = users.stream()
    .flatMap(user -> Stream.of(user.getName()))  // Избыточно
    .collect(Collectors.toList());

// Хорошо: просто map
List<String> names = users.stream()
    .map(User::getName)
    .collect(Collectors.toList());

// Плохо: flatMap для фильтрации
List<User> active = users.stream()
    .flatMap(user -> user.isActive() ? Stream.of(user) : Stream.empty())
    .collect(Collectors.toList());

// Хорошо: filter
List<User> active = users.stream()
    .filter(User::isActive)
    .collect(Collectors.toList());

Резюме

flatMap идеален когда:

  • Нужно развернуть вложенные структуры (List<List<T>>)
  • Работаешь с Optional, Stream, CompletableFuture цепочками
  • Нужно одновременно трансформировать и развернуть данные
  • Обрабатываешь коллекции коллекций

Это мощный инструмент для функционального программирования в Java, который делает код компактнее и выразительнее.