← Назад к вопросам
Как хранятся несколько значений с одним ключом
1.3 Junior🔥 71 комментариев
#Коллекции
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Хранение нескольких значений с одним ключом в Java
Это практическая задача, которая часто возникает в разработке. Есть несколько стандартных подходов, каждый со своими преимуществами.
Проблема
// ❌ Проблема: обычная Map хранит только одно значение на ключ
Map<String, String> map = new HashMap<>();
map.put("tags", "java");
map.put("tags", "spring"); // Перезаписывает предыдущее значение
System.out.println(map.get("tags")); // Выведет только "spring"
Решение 1: Map со списком значений (КЛАССИЧЕСКИЙ ПОДХОД)
Самый простой и понятный способ — использовать Map<K, List<V>>:
public class MultiValueMap {
private Map<String, List<String>> map = new HashMap<>();
public void put(String key, String value) {
// Если ключа нет, создаём новый список
map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
}
public List<String> get(String key) {
return map.getOrDefault(key, Collections.emptyList());
}
public void remove(String key) {
map.remove(key);
}
public void removeValue(String key, String value) {
List<String> values = map.get(key);
if (values != null) {
values.remove(value);
// Если список пуст, удаляем ключ
if (values.isEmpty()) {
map.remove(key);
}
}
}
}
// Использование
MultiValueMap map = new MultiValueMap();
map.put("tags", "java");
map.put("tags", "spring");
map.put("tags", "hibernate");
List<String> tags = map.get("tags");
System.out.println(tags); // [java, spring, hibernate]
Решение 2: Apache Commons MultiValueMap
Апач предоставляет готовую реализацию:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.multimap.ArrayListValuedHashMap;
public class MultiValueMapExample {
public static void main(String[] args) {
MultiValuedMap<String, String> map = new ArrayListValuedHashMap<>();
map.put("tags", "java");
map.put("tags", "spring");
map.put("tags", "hibernate");
// Получить все значения
Collection<String> tags = map.get("tags");
System.out.println(tags); // [java, spring, hibernate]
// Размер всех значений для ключа
int count = map.size(); // 3
// Содержит ли значение
boolean contains = map.containsValue("spring"); // true
// Удалить конкретное значение
map.removeMapping("tags", "spring");
}
}
Решение 3: Guava Multimap
Google Guava также предоставляет элегантное решение:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.0.0-jre</version>
</dependency>
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
public class GuavaMultimapExample {
public static void main(String[] args) {
// Мутабельный multimap
Multimap<String, String> map = ArrayListMultimap.create();
map.put("tags", "java");
map.put("tags", "spring");
map.put("tags", "hibernate");
// Все значения
Collection<String> tags = map.get("tags");
System.out.println(tags); // [java, spring, hibernate]
// Количество значений для ключа
int size = map.size(); // 3
// Вспомогательные операции
System.out.println(map.keys()); // Multiset [tags, tags, tags]
System.out.println(map.values()); // Collection [java, spring, hibernate]
System.out.println(map.entries()); // Entries
// Удалить конкретное значение
map.remove("tags", "spring");
}
}
Иммутабельный Multimap:
import com.google.common.collect.ImmutableListMultimap;
ImmutableListMultimap<String, String> map =
ImmutableListMultimap.<String, String>builder()
.put("tags", "java")
.put("tags", "spring")
.put("tags", "hibernate")
.build();
// Создание из обычного map
Map<String, String> data = new HashMap<>();
ImmutableListMultimap<String, String> immutable =
Multimaps.index(data.entrySet(), Map.Entry::getKey);
Решение 4: Map<K, Set<V>> (без дубликатов)
Если не нужны дубликаты, используем Set:
public class MultiValueSetMap {
private Map<String, Set<String>> map = new HashMap<>();
public void put(String key, String value) {
map.computeIfAbsent(key, k -> new HashSet<>()).add(value);
}
public Set<String> get(String key) {
return map.getOrDefault(key, Collections.emptySet());
}
public boolean contains(String key, String value) {
Set<String> values = map.get(key);
return values != null && values.contains(value);
}
}
// Использование
MultiValueSetMap map = new MultiValueSetMap();
map.put("tags", "java");
map.put("tags", "java"); // Не добавится (дубликат)
map.put("tags", "spring");
Set<String> tags = map.get("tags");
System.out.println(tags); // [java, spring] (без дубликатов)
Решение 5: Map<K, Set<V>> с TreeSet (сортировка)
Для упорядоченных значений:
public class SortedMultiValueMap {
private Map<String, Set<String>> map = new HashMap<>();
public void put(String key, String value) {
// Используем TreeSet для автоматической сортировки
map.computeIfAbsent(key, k -> new TreeSet<>()).add(value);
}
public Set<String> get(String key) {
return map.getOrDefault(key, Collections.emptySortedSet());
}
}
// Использование
SortedMultiValueMap map = new SortedMultiValueMap();
map.put("tags", "spring");
map.put("tags", "java");
map.put("tags", "hibernate");
Set<String> tags = map.get("tags");
System.out.println(tags); // [hibernate, java, spring] - отсортировано
Решение 6: Использование Stream API для трансформации
Трансформирование коллекции в multimap:
import java.util.stream.Collectors;
public class StreamToMultimap {
public static void main(String[] args) {
// Список пользователей
List<User> users = Arrays.asList(
new User("john", "admin"),
new User("jane", "user"),
new User("bob", "admin"),
new User("alice", "user")
);
// Группируем по роли с использованием Collectors.groupingBy
Map<String, List<User>> usersByRole = users.stream()
.collect(Collectors.groupingBy(User::getRole));
System.out.println(usersByRole);
// {admin=[john, bob], user=[jane, alice]}
// Группируем и трансформируем значения
Map<String, List<String>> namesByRole = users.stream()
.collect(Collectors.groupingBy(
User::getRole,
Collectors.mapping(User::getName, Collectors.toList())
));
System.out.println(namesByRole);
// {admin=[john, bob], user=[jane, alice]}
// Группируем в Set (без дубликатов)
Map<String, Set<String>> uniqueNamesByRole = users.stream()
.collect(Collectors.groupingBy(
User::getRole,
Collectors.mapping(User::getName, Collectors.toSet())
));
}
static class User {
String name;
String role;
User(String name, String role) { this.name = name; this.role = role; }
String getName() { return name; }
String getRole() { return role; }
}
}
Решение 7: Использование groupingByConcurrent для параллельной обработки
import java.util.stream.Collectors;
Map<String, List<User>> usersByRole = users.parallelStream()
.collect(Collectors.groupingByConcurrent(
User::getRole,
Collectors.toList()
));
Решение 8: Собственная реализация с пользовательскими операциями
public class CustomMultiMap<K, V> {
private Map<K, List<V>> map = new HashMap<>();
public void put(K key, V value) {
map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
}
public void putAll(K key, Collection<V> values) {
map.computeIfAbsent(key, k -> new ArrayList<>()).addAll(values);
}
public List<V> get(K key) {
return map.getOrDefault(key, Collections.emptyList());
}
public void remove(K key) {
map.remove(key);
}
public void removeValue(K key, V value) {
List<V> values = map.get(key);
if (values != null && values.remove(value)) {
if (values.isEmpty()) {
map.remove(key);
}
}
}
public boolean contains(K key, V value) {
List<V> values = map.get(key);
return values != null && values.contains(value);
}
public int size(K key) {
return map.getOrDefault(key, Collections.emptyList()).size();
}
public boolean isEmpty() {
return map.isEmpty();
}
// Для итерации по всем парам
public void forEach(BiConsumer<K, V> consumer) {
map.forEach((key, values) -> values.forEach(value -> consumer.accept(key, value)));
}
}
// Использование
CustomMultiMap<String, String> map = new CustomMultiMap<>();
map.put("tags", "java");
map.put("tags", "spring");
map.putAll("skills", Arrays.asList("coding", "design", "testing"));
map.forEach((key, value) ->
System.out.println(key + ": " + value)
);
Сравнение подходов
| Подход | Дубликаты | Порядок | Простота | Зависимости |
|---|---|---|---|---|
| Map<K, List<V>> | ✅ | сохраняет | ⭐⭐⭐ | нет |
| Map<K, Set<V>> | ❌ | не гарантирует | ⭐⭐⭐ | нет |
| Map<K, TreeSet<V>> | ❌ | отсортирован | ⭐⭐⭐ | нет |
| Apache MultiValueMap | ✅ | сохраняет | ⭐⭐ | commons-collections |
| Guava Multimap | ✅ | сохраняет | ⭐⭐ | guava |
| Stream groupingBy | ✅ | сохраняет | ⭐⭐ | встроено |
Рекомендации
Выбирайте в зависимости от ситуации:
- Нет зависимостей? →
Map<K, List<V>>сcomputeIfAbsent - Дубликаты нежелательны? →
Map<K, Set<V>> - Нужна сортировка? →
Map<K, TreeSet<V>> - Работаете с Guava? →
Multimap - Apache Commons? →
MultiValuedMap - Трансформация коллекции? →
Stream.groupingBy
Мой личный выбор для production кода:
// Простое хранилище
Map<String, List<String>> map = new HashMap<>();
map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
// Если надо в Guava проекте
Multimap<String, String> map = ArrayListMultimap.create();
// Если трансформируем коллекцию
Map<String, List<Item>> grouped = items.stream()
.collect(Collectors.groupingBy(Item::getCategory));