Java 8: Преобразование List в Map с помощью Streams
Узнайте, как преобразовывать List<V> в Map<K, V> с помощью Java 8 streams и lambdas без внешних библиотек. Полное руководство с примерами, обработкой дубликатов ключей и советами по производительности.
Как преобразовать List
Я ищу реализацию на Java 8 для преобразования списка объектов в карту, аналогично тому, как я бы сделал это в Java 7:
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:
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:
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 потоков и лямбда-выражений
Для преобразования ListCollectors.toMap(). Это стандартный подход в Java 8, который заменяет ручные циклы for и Maps.uniqueIndex() из Guava.
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
- Обработка дубликатов ключей
- Пользовательские типы Map и значения null
- Полный пример
- Соображения по производительности
- Распространенные случаи использования
Базовое преобразование List в Map
Простейшая форма Collectors.toMap() принимает две функции:
- Функция извлечения ключа: извлекает ключ из каждого элемента
- Функция извлечения значения: извлекает значение из каждого элемента
// Преобразование 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. Сохранить первое вхождение
Map<String, Choice> choiceMap = choices.stream()
.collect(Collectors.toMap(
Choice::getName,
Function.identity(),
(existing, replacement) -> existing // Сохранить первое вхождение
));
2. Сохранить последнее вхождение
Map<String, Choice> choiceMap = choices.stream()
.collect(Collectors.toMap(
Choice::getName,
Function.identity(),
(existing, replacement) -> replacement // Сохранить последнее вхождение
));
3. Объединить значения
Если вы хотите объединить значения при дублировании ключей:
// Конкатенация имен для дублирующихся ключей
Map<String, String> combinedNames = choices.stream()
.collect(Collectors.toMap(
Choice::getName,
Choice::getName,
(existing, replacement) -> existing + ", " + replacement
));
Пользовательские типы Map и значения null
1. Указать тип Map (например, LinkedHashMap)
Чтобы сохранить порядок вставки, можно указать тип карты:
Map<String, Choice> orderedChoiceMap = choices.stream()
.collect(Collectors.toMap(
Choice::getName,
Function.identity(),
(existing, replacement) -> existing,
LinkedHashMap::new // Сохранять порядок вставки
));
2. Обработка null-ключей
Если ваша функция извлечения ключей может возвращать null, можно использовать Collectors.toConcurrentMap() или отфильтровать null-ключи:
Map<String, Choice> filteredChoiceMap = choices.stream()
.filter(choice -> choice.getName() != null)
.collect(Collectors.toMap(
Choice::getName,
Function.identity(),
(existing, replacement) -> existing
));
Полный пример
Вот комплексный пример, демонстрирующий различные сценарии преобразования:
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 в плане:
- Читаемость: Более лаконично и декларативно
- Параллельная обработка: Может быть легко распараллелен с помощью
.parallelStream() - Эффективность использования памяти: Потоки могут обрабатывать элементы лениво
Однако для очень небольших коллекций разница в производительности незначительна. Согласно сравнению Baeldung, подход с потоками показывает схожую производительность с ручной итерацией в большинстве сценариев.
Распространенные случаи использования
1. Преобразование списка объектов в карту по ID
List<User> users = ...;
Map<Integer, User> userById = users.stream()
.collect(Collectors.toMap(User::getId, Function.identity()));
2. Преобразование списка в карту с разными типами значений
List<Product> products = ...;
Map<String, Double> productPriceMap = products.stream()
.collect(Collectors.toMap(
Product::getName,
Product::getPrice
));
3. Преобразование списка в карту со сложными ключами
List<Order> orders = ...;
Map<String, List<Order>> ordersByCustomer = orders.stream()
.collect(Collectors.groupingBy(
order -> order.getCustomer().getLastName() + ", " + order.getCustomer().getFirstName()
));
Подход с использованием Java 8 потоков обеспечивает чистый и эффективный способ преобразования списков в карты без внешних зависимостей, при этом предлагая встроенную обработку дубликатов ключей и различных типов коллекций.