Другое

Как расплющить List<List<Object>> в Java 8

Узнайте, как преобразовать List<List<Object>> в один List<Object> в Java 8 с помощью flatMap(). Полное руководство с примерами и оценкой производительности.

Как можно «расплющить» List<List<Object>> в один List<Object> в Java 8?
Если у меня есть List<List<Object>>, как преобразовать его в List<Object>, содержащий все объекты в том же порядке итерации, используя возможности Java 8?

Чтобы преобразовать List<List<Object>> в один List<Object> с сохранением порядка итерации в Java 8, можно воспользоваться методом flatMap() из API Stream. Этот подход эффективно преобразует поток списков в один поток, содержащий все элементы из всех вложенных списков в их исходном порядке.

Содержание

Использование flatMap() для развёртывания

Самый элегантный и идиоматический способ развёртывания вложенных коллекций в Java 8 – это использование метода flatMap(). Он принимает функцию, которая преобразует каждый элемент в поток, а затем «расплющивает» все полученные потоки в один поток.

java
List<List<Object>> nestedList = ...; // Ваш вложенный список
List<Object> flattenedList = nestedList.stream()
    .flatMap(List::stream)           // Разворачиваем каждый вложенный список
    .collect(Collectors.toList());   // Собираем в новый список

Ключевая идея состоит в том, что flatMap() преобразует Stream<List<Object>> в Stream<Object> путём применения list.stream() к каждому элементу и последующего объединения всех потоков.

Полный пример с кодом

Ниже приведён практический пример, демонстрирующий развёртывание списка списков:

java
import java.util.*;
import java.util.stream.*;

public class ListFlatteningExample {
    public static void main(String[] args) {
        // Создаём структуру вложенного списка
        List<List<Integer>> listOfLists = Arrays.asList(
            Arrays.asList(1, 2, 3),
            Arrays.asList(4, 5),
            Arrays.asList(6, 7, 8, 9)
        );
        
        // Разворачиваем с помощью flatMap
        List<Integer> flattenedList = listOfLists.stream()
            .flatMap(List::stream)
            .collect(Collectors.toList());
            
        System.out.println("Оригинальный вложенный список: " + listOfLists);
        System.out.println("Развёрнутый список: " + flattenedList);
        // Вывод: Развёрнутый список: [1, 2, 3, 4, 5, 6, 7, 8, 9]
    }
}

Согласно статье HowToDoInJava, данный подход работает, объединяя все объекты из коллекций в исходном потоке в одну коллекцию через операцию развёртывания.

Альтернативные подходы

Хотя flatMap() является предпочтительным методом, существуют и другие варианты:

1. Использование forEach с Collection.addAll()

java
List<Object> result = new ArrayList<>();
nestedList.forEach(result::addAll);

Этот подход более императивный, но в некоторых сценариях может быть чуть быстрее.

2. Использование Stream.toList() в Java 9+

Если вы используете Java 9 или новее, можно воспользоваться более лаконичным методом toList():

java
List<Object> flattenedList = nestedList.stream()
    .flatMap(List::stream)
    .toList();  // Функция доступна с Java 9

3. Использование reduce()

java
List<Object> flattenedList = nestedList.stream()
    .reduce(new ArrayList<>(), 
        (list1, list2) -> {
            list1.addAll(list2);
            return list1;
        });

Однако, как отмечено в Stack Overflow, flatMap обычно предпочтительнее, поскольку не создаёт промежуточные объекты Stream.

Проблемы производительности

У метода flatMap() есть несколько характеристик, которые стоит учитывать:

  • Память: каждый вызов list.stream() создаёт новый объект Stream, что может добавить накладные расходы при работе с очень большими коллекциями.
  • Параллельная обработка: flatMap() хорошо работает с параллельными потоками, что делает его подходящим для больших наборов данных.
  • Сохранение порядка: порядок итерации сохраняется, что делает поведение предсказуемым.

Как отмечено в исследовании от Baeldung, функциональный подход с flatMap обычно более читаемый и поддерживаемый, чем традиционные императивные методы.

Обработка крайних случаев

При работе с операциями развёртывания стоит учитывать следующие крайние случаи:

Пустые списки

java
List<List<Object>> nestedList = Arrays.asList(
    Collections.emptyList(),
    Arrays.asList(1, 2),
    Collections.emptyList(),
    Arrays.asList(3)
);

List<Object> flattened = nestedList.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList());
// Результат: [1, 2, 3] – пустые списки автоматически пропускаются

Null‑элементы

Если вложенный список может содержать null:

java
List<List<Object>> nestedList = Arrays.asList(
    Arrays.asList("a", "b"),
    null,  // Null‑подсписок
    Arrays.asList("c", "d")
);

List<Object> flattened = nestedList.stream()
    .filter(Objects::nonNull)  // Фильтруем null‑подсписки
    .flatMap(List::stream)
    .collect(Collectors.toList());

Null‑элементы внутри подсписков

java
List<String> flattened = nestedList.stream()
    .flatMap(list -> list == null ? Stream.empty() : list.stream())
    .collect(Collectors.toList());

Сравнение с традиционными методами

Традиционные императивные подходы к развёртыванию вложенных списков включают:

Вложенные циклы for

java
List<Object> result = new ArrayList<>();
for (List<Object> sublist : nestedList) {
    for (Object item : sublist) {
        result.add(item);
    }
}

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

java
List<Object> result = new ArrayList<>();
for (List<Object> sublist : nestedList) {
    result.addAll(sublist);
}

Хотя эти подходы работают, метод flatMap() предлагает несколько преимуществ:

  • Более лаконичный: требует меньше шаблонного кода.
  • Более функциональный: соответствует принципам функционального программирования.
  • Более композиционный: легко комбинируется с другими операциями Stream.
  • Более выразительный: явно передаёт намерение «разворачивать» структуру.

Согласно Java67, функция flatMap() специально предназначена для преобразования Stream коллекций в Stream отдельных элементов, что делает её идеальным инструментом для этой операции.

Заключение

  1. Основное решение: используйте flatMap(List::stream) в сочетании с collect(Collectors.toList()) для самого лаконичного и читаемого подхода.
  2. Сохранение порядка: порядок итерации сохраняется, элементы появляются в том же порядке, что и при вложенной итерации.
  3. Производительность: для большинства случаев flatMap() обеспечивает хороший баланс между читаемостью и производительностью.
  4. Крайние случаи: не забывайте обрабатывать null‑подсписки в реальных приложениях.
  5. Альтернатива: для критических участков кода, где нужна максимальная производительность, рассмотрите императивный подход forEach(result::addAll).

Метод flatMap() остаётся самым идиоматичным и рекомендуемым способом развёртывания вложенных коллекций в Java 8, обеспечивая как ясность, так и эффективность для этой распространённой операции.

Источники

  1. Java Stream flatMap() with Examples - HowToDoInJava
  2. Flattening Nested Collections in Java - Baeldung
  3. Java 8 Stream + FlatMap Example for Beginners - Java67
  4. How can I turn a List of Lists into a List in Java 8? - Stack Overflow
  5. Stream flatMap() in Java with examples - GeeksforGeeks
Авторы
Проверено модерацией
Модерация