Avatar ·

Как преобразовать список списков (List<List<T>>) в плоский список (List<T>) в Java 8?

Всем привет! Я только начал осваивать Java 8 и Stream API, и столкнулся с такой задачей. У меня есть структура данных, которая представляет собой список списков строк. Например, я получаю данные из CSV-файла, где каждая строка — это список полей, а весь файл — список таких строк. Мне нужно объединить все эти строки в один общий список, то есть "развернуть" вложенную структуру. Вот упрощённый пример того, что у меня есть: ```java List> listOfLists = Arrays.asList( Arrays.asList("a", "b", "c"), Arrays.asList("d", "e"), Arrays.asList("f", "g", "h", "i") ); ``` Я хочу получить на выходе: ```java List flatList = Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h", "i"); ``` Я знаю, что в старом Java (до 8) это можно было сделать через вложенные циклы for-each, но это громоздко. Я слышал, что в Java 8 есть какой-то элегантный способ сделать это с помощью стримов, но никак не могу разобраться. Пробовал что-то вроде `listOfLists.stream().collect(...)`, но не понимаю, какой коллектор использовать. Видел метод `flatMap`, но не совсем понимаю, как он работает и как его правильно применить к моей задаче. Подскажите, пожалуйста, как это сделать правильно и красиво? Желательно с пояснением, что именно происходит на каждом шаге. Заранее спасибо! <<>>

Решение с помощью Stream API и flatMap

Ваша задача решается в одну строку с помощью метода flatMap из Stream API. Вот как это выглядит:

List<String> flatList = listOfLists.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList());

Пошаговое объяснение

Давайте разберём, что здесь происходит:

  1. listOfLists.stream() — создаёт поток из вашего исходного списка списков. Тип этого потока — Stream<List<String>>. Каждый элемент потока — это внутренний список (например, ["a", "b", "c"]).
  2. .flatMap(List::stream) — это ключевой момент. Метод flatMap делает две вещи:
    • Применяет функцию к каждому элементу потока. В нашем случае функция List::stream (это ссылка на метод) преобразует каждый внутренний список в поток его элементов (Stream<String>).
    • "Склеивает" (flatten) все полученные потоки в один общий поток.

    Визуально это выглядит так: было [ [a,b,c], [d,e], [f,g,h,i] ], а стало [ a, b, c, d, e, f, g, h, i ].

  3. .collect(Collectors.toList()) — собирает все элементы получившегося плоского потока обратно в список List<String>.

Полный рабочий пример

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

public class FlattenListExample {
    public static void main(String[] args) {
        List<List<String>> listOfLists = Arrays.asList(
            Arrays.asList("a", "b", "c"),
            Arrays.asList("d", "e"),
            Arrays.asList("f", "g", "h", "i")
        );

        List<String> flatList = listOfLists.stream()
            .flatMap(List::stream)
            .collect(Collectors.toList());

        System.out.println(flatList); // [a, b, c, d, e, f, g, h, i]
    }
}

Варианты и нюансы

  • Лямбда вместо ссылки на метод: можно написать .flatMap(list -> list.stream()) — это то же самое, что .flatMap(List::stream).
  • Работа с примитивами: если у вас List<List<Integer>>, то для лучшей производительности можно использовать flatMapToInt:
    listOfLists.stream().flatMapToInt(list -> list.stream().mapToInt(Integer::intValue)).toArray()
  • Обработка null: если внутри списка могут быть null элементы, используйте flatMap(list -> list == null ? Stream.empty() : list.stream()).
  • Сортировка и фильтрация: вы можете комбинировать flatMap с другими операциями:
    listOfLists.stream().flatMap(List::stream).filter(s -> s.length() > 1).sorted().collect(Collectors.toList())

Что делать, если нужно сохранить порядок?

Потоки по умолчанию обрабатывают элементы последовательно, если вы не вызываете .parallelStream(). В нашем примере порядок сохранится: сначала все элементы первого списка, затем второго и т.д.

Этот подход — стандартный и самый читаемый способ преобразования списка списков в плоский список в Java 8. Он лаконичен, эффективен и хорошо сочетается с другими операциями Stream API.

<<>>

Альтернативный подход: использование reduce или старый добрый цикл

Хотя flatMap — самый идиоматичный способ, есть и другие варианты. Например, можно использовать reduce для объединения всех списков:

List<String> flatList = listOfLists.stream()
    .reduce(new ArrayList<>(), (list1, list2) -> {
        list1.addAll(list2);
        return list1;
    });

Однако этот подход менее эффективен, так как при каждом слиянии создаётся новый список (или модифицируется существующий), и он не рекомендуется для больших объёмов данных.

Если вы не используете Java 8 или предпочитаете более явный код, можно вернуться к классическому решению с вложенными циклами:

List<String> flatList = new ArrayList<>();
for (List<String> innerList : listOfLists) {
    flatList.addAll(innerList);
}

Этот код абсолютно прозрачен, легко читается и не требует знания Stream API. Он идеально подходит, если ваша кодовая база ещё не перешла на Java 8 или если вы хотите избежать оверхэда от стримов при работе с очень большими объёмами данных (хотя на практике разница обычно незначительна).

Также стоит упомянуть библиотеку Guava от Google, которая предоставляет утилиту Iterables.concat() для объединения коллекций без создания промежуточных копий.