Другое

Java 8: Преобразование List в Map с помощью Streams

Узнайте, как преобразовывать List<V> в Map<K, V> с помощью Java 8 streams и lambdas без внешних библиотек. Полное руководство с примерами, обработкой дубликатов ключей и советами по производительности.

Как преобразовать List в Map<K, V> с использованием Java 8 streams и lambdas без внешних библиотек?

Я ищу реализацию на Java 8 для преобразования списка объектов в карту, аналогично тому, как я бы сделал это в Java 7:

java
private Map<String, Choice> nameMap(List<Choice> choices) {
    final Map<String, Choice> hashMap = new HashMap<>();
    for (final Choice choice : choices) {
        hashMap.put(choice.getName(), choice);
    }
    
    return hashMap;
}

Я знаю, что это можно сделать с помощью Guava:

java
private Map<String, Choice> nameMap(List<Choice> choices) {
    return Maps.uniqueIndex(choices, new Function<Choice, String>() {
        @Override
        public String apply(final Choice input) {
            return input.getName();
        }
    });
}

И с Guava и Java 8 lambdas:

java
private Map<String, Choice> nameMap(List<Choice> choices) {
    return Maps.uniqueIndex(choices, Choice::getName);
}

Однако я хочу реализовать это, используя только Java 8 streams и lambdas, не полагаясь на внешние библиотеки, такие как Guava. Какой самый эффективный способ достичь этого преобразования?

Преобразование List в Map<K, V> с использованием Java 8 потоков и лямбда-выражений

Для преобразования List в Map<K, V> с использованием Java 8 потоков и лямбда-выражений без внешних библиотек можно использовать метод Collectors.toMap(). Это стандартный подход в Java 8, который заменяет ручные циклы for и Maps.uniqueIndex() из Guava.

java
private Map<String, Choice> nameMap(List<Choice> choices) {
    return choices.stream()
        .collect(Collectors.toMap(Choice::getName, Function.identity()));
}

Эта реализация эквивалентна вашему коду на Java 7, но более лаконична и функциональна. Choice::getName - это ссылка на метод, которая извлекает ключ из каждого объекта Choice, а Function.identity() возвращает сам объект в качестве значения.


Содержание


Базовое преобразование List в Map

Простейшая форма Collectors.toMap() принимает две функции:

  1. Функция извлечения ключа: извлекает ключ из каждого элемента
  2. Функция извлечения значения: извлекает значение из каждого элемента
java
// Преобразование List<Choice> в Map<String, Choice>, где ключ - имя выбора
Map<String, Choice> choiceMap = choices.stream()
    .collect(Collectors.toMap(Choice::getName, Function.identity()));

По умолчанию создается HashMap, и будет выброшено исключение IllegalStateException при обнаружении дубликатов ключей. Согласно документации Baeldung, это наиболее базовая реализация преобразования списка в карту.


Обработка дубликатов ключей

Когда в вашем списке могут быть дублирующиеся ключи, необходимо указать функцию слияния. Метод Collectors.toMap() имеет перегрузку, которая принимает BinaryOperator для разрешения конфликтов:

1. Сохранить первое вхождение

java
Map<String, Choice> choiceMap = choices.stream()
    .collect(Collectors.toMap(
        Choice::getName, 
        Function.identity(),
        (existing, replacement) -> existing  // Сохранить первое вхождение
    ));

2. Сохранить последнее вхождение

java
Map<String, Choice> choiceMap = choices.stream()
    .collect(Collectors.toMap(
        Choice::getName, 
        Function.identity(),
        (existing, replacement) -> replacement  // Сохранить последнее вхождение
    ));

3. Объединить значения

Если вы хотите объединить значения при дублировании ключей:

java
// Конкатенация имен для дублирующихся ключей
Map<String, String> combinedNames = choices.stream()
    .collect(Collectors.toMap(
        Choice::getName,
        Choice::getName,
        (existing, replacement) -> existing + ", " + replacement
    ));

Пользовательские типы Map и значения null

1. Указать тип Map (например, LinkedHashMap)

Чтобы сохранить порядок вставки, можно указать тип карты:

java
Map<String, Choice> orderedChoiceMap = choices.stream()
    .collect(Collectors.toMap(
        Choice::getName,
        Function.identity(),
        (existing, replacement) -> existing,
        LinkedHashMap::new  // Сохранять порядок вставки
    ));

2. Обработка null-ключей

Если ваша функция извлечения ключей может возвращать null, можно использовать Collectors.toConcurrentMap() или отфильтровать null-ключи:

java
Map<String, Choice> filteredChoiceMap = choices.stream()
    .filter(choice -> choice.getName() != null)
    .collect(Collectors.toMap(
        Choice::getName,
        Function.identity(),
        (existing, replacement) -> existing
    ));

Полный пример

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

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

class Choice {
    private String name;
    private String value;
    
    public Choice(String name, String value) {
        this.name = name;
        this.value = value;
    }
    
    public String getName() { return name; }
    public String getValue() { return value; }
    
    @Override
    public String toString() {
        return "Choice{name='" + name + "', value='" + value + "'}";
    }
}

public class ListToMapConverter {
    
    // Базовое преобразование
    public Map<String, Choice> toBasicMap(List<Choice> choices) {
        return choices.stream()
            .collect(Collectors.toMap(Choice::getName, Function.identity()));
    }
    
    // Обработка дубликатов путем сохранения первого вхождения
    public Map<String, Choice> toMapKeepFirst(List<Choice> choices) {
        return choices.stream()
            .collect(Collectors.toMap(
                Choice::getName,
                Function.identity(),
                (existing, replacement) -> existing
            ));
    }
    
    // Обработка дубликатов путем сохранения последнего вхождения
    public Map<String, Choice> toMapKeepLast(List<Choice> choices) {
        return choices.stream()
            .collect(Collectors.toMap(
                Choice::getName,
                Function.identity(),
                (existing, replacement) -> replacement
            ));
    }
    
    // Создание LinkedHashMap для сохранения порядка
    public Map<String, Choice> toOrderedMap(List<Choice> choices) {
        return choices.stream()
            .collect(Collectors.toMap(
                Choice::getName,
                Function.identity(),
                (existing, replacement) -> existing,
                LinkedHashMap::new
            ));
    }
    
    public static void main(String[] args) {
        List<Choice> choices = Arrays.asList(
            new Choice("A", "Value1"),
            new Choice("B", "Value2"),
            new Choice("A", "Value3"),  // Дублирующийся ключ
            new Choice("C", "Value4")
        );
        
        ListToMapConverter converter = new ListToMapConverter();
        
        System.out.println("Базовое преобразование (выбрасывает исключение при дубликатах):");
        try {
            System.out.println(converter.toBasicMap(choices));
        } catch (IllegalStateException e) {
            System.out.println("Ошибка: " + e.getMessage());
        }
        
        System.out.println("\nСохранить первое вхождение:");
        System.out.println(converter.toMapKeepFirst(choices));
        
        System.out.println("\nСохранить последнее вхождение:");
        System.out.println(converter.toMapKeepLast(choices));
        
        System.out.println("\nУпорядоченная карта:");
        System.out.println(converter.toOrderedMap(choices));
    }
}

Соображения по производительности

Подход с использованием Java 8 потоков и Collectors.toMap() обычно более эффективен, чем ручные циклы for в плане:

  1. Читаемость: Более лаконично и декларативно
  2. Параллельная обработка: Может быть легко распараллелен с помощью .parallelStream()
  3. Эффективность использования памяти: Потоки могут обрабатывать элементы лениво

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


Распространенные случаи использования

1. Преобразование списка объектов в карту по ID

java
List<User> users = ...;
Map<Integer, User> userById = users.stream()
    .collect(Collectors.toMap(User::getId, Function.identity()));

2. Преобразование списка в карту с разными типами значений

java
List<Product> products = ...;
Map<String, Double> productPriceMap = products.stream()
    .collect(Collectors.toMap(
        Product::getName,
        Product::getPrice
    ));

3. Преобразование списка в карту со сложными ключами

java
List<Order> orders = ...;
Map<String, List<Order>> ordersByCustomer = orders.stream()
    .collect(Collectors.groupingBy(
        order -> order.getCustomer().getLastName() + ", " + order.getCustomer().getFirstName()
    ));

Подход с использованием Java 8 потоков обеспечивает чистый и эффективный способ преобразования списков в карты без внешних зависимостей, при этом предлагая встроенную обработку дубликатов ключей и различных типов коллекций.

Авторы
Проверено модерацией
Модерация