HashMap: примеры (JAVA)
HashMapОбщее описание класса HashMap
Класс HashMap из пакета java.util представляет реализацию отображения на основе хеш-таблицы. Используется для хранения пар "ключ-значение" с быстрым доступом по ключу. Ключи и значения могут быть null (ключ null допускается в единственном экземпляре). Коллекция не гарантирует порядок элементов. Не является потоко-безопасной; при многопоточном доступе требуется внешняя синхронизация или применение ConcurrentHashMap.
Ниже перечиснены основные конструкторы и методы с описанием их аргументов и возвращаемых значений.
- Конструкторы:
HashMap()- создаёт пустую карту с начальной ёмкостью 16 и коэффициентом загрузки 0.75f.HashMap(int initialCapacity)- создаёт карту с заданной начальной ёмкостью. Аргумент: положительное целое. При отрицательном значении выбрасывается IllegalArgumentException.HashMap(int initialCapacity, float loadFactor)- задаёт начальную ёмкость и коэффициент загрузки (loadFactor должен быть положительным и конечным). Возвращаемого значения нет, при недопустимых аргументах - IllegalArgumentException.HashMap(Map extends K,? extends V> m)- создаёт карту и копирует пары из заданной карты. Если m == null - NullPointerException.
- Основные методы:
V put(K key, V value)- сохраняет значение value по ключу key. Возвращает предыдущее значение для ключа или null, если его не было (или если предыдущее значение было null). Аргументы: ключ и значение могут быть null.V get(Object key)- возвращает значение по ключу или null, если ключ отсутствует. Для различия отсутствия и значения null используется containsKey.V remove(Object key)- удаляет привязку по ключу. Возвращает удалённое значение или null, если ключа не было.boolean containsKey(Object key)- true, если карта содержит указанное соответствие ключа.boolean containsValue(Object value)- true, если карта содержит хотя бы одно соответствие со значением value (линейный обход значений).int size()- число пар в карте.boolean isEmpty()- true, если нет пар.void clear()- удаление всех пар. Ничего не возвращает.Set- представление ключей; изменения в представлении отражаются в карте.keySet() Collection- представление значений.values() Set- набор записей, поддерживающий итерацию и удаление через итератор.> entrySet() V putIfAbsent(K key, V value)- добавляет пару только если ключа нет или значение равно null; возвращает предыдущее значение или null. (Добавлен как метод интерфейса Map в JDK 8.)V getOrDefault(Object key, V defaultValue)- возвращает значение или defaultValue, если ключ отсутствует.V replace(K key, V value)иboolean replace(K key, V oldValue, V newValue)- замена существующего значения; возвращают предыдущее значение либо булево для версии с проверкой старого значения.void replaceAll(BiFunction super K,? super V,? extends V> function)- применяет функцию ко всем записям, заменяя значения.V computeIfAbsent(K key, Function super K,? extends V> mappingFunction)- если ключа нет, вычисляет и сохраняет значение; возвращает текущее значение.V computeIfPresent(K key, BiFunction super K,? super V,? extends V> remappingFunction)- при наличии ключа пересчитывает значение; если функция возвращает null, запись удаляется.V compute(K key, BiFunction super K,? super V,? extends V> remappingFunction)- универсальная операция пересчёта; поведение аналогично: если возвращается null, запись удаляется.V merge(K key, V value, BiFunction super V,? super V,? extends V> remappingFunction)- объединяет существующее и новое значение по ключу: если значения нет, ставит value; иначе применяет функцию и сохраняет результат (null удаляет запись).Object clone()- поверхностная копия HashMap; ключи и значения не клонируются.
- Внутренние параметры:
initialCapacity- начальная ёмкость хранилища (число корзин), рекомендуется выбирать с запасом для уменьшения рехешинга.loadFactor- коэффициент загрузки, при превышении которого размер таблицы удваивается: newCapacity = oldCapacity * 2. Типичные значения: 0.75f (сбалансировано по памяти и скорости).- В JDK 8 и выше при сильных коллизиях бакеты преобразуются в сбалансированные деревья для улучшения производительности поиска.
Короткие примеры использования
Ниже показаны простые варианты операций создания и доступа. Код компилируется с JDK 8 и выше.
Пример 1. Базовые put/get/remove
import java.util.HashMap;
public class Ex1 {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);
System.out.println(map.get("a"));
System.out.println(map.remove("b"));
System.out.println(map.size());
}
}
1 2 1
Пример 2. null ключ и значения
HashMap<String, String> m = new HashMap<>();
m.put(null, "x");
m.put("k", null);
System.out.println(m);
{null=x, k=null}
Пример 3. putIfAbsent и getOrDefault
HashMap<String, Integer> counts = new HashMap<>();
counts.putIfAbsent("apple", 0);
counts.putIfAbsent("apple", 5);
System.out.println(counts.getOrDefault("apple", -1));
System.out.println(counts.getOrDefault("banana", -1));
0 -1
Пример 4. итерация по entrySet
for (Map.Entry<String, Integer> e : counts.entrySet()) {
System.out.println(e.getKey() + "=" + e.getValue());
}
apple=0
Похожие реализации в Java
- LinkedHashMap - сохраняет порядок вставки или порядок доступа. Предпочтительна при необходимости детерминированного порядка.
- TreeMap - реализует отсортированное отображение на основе красно-черного дерева; ключи требуют сравнения. Предпочтительна при необходимости упорядоченного вывода.
- ConcurrentHashMap - потокобезопасная реализация с высокой производительностью для многопоточного доступа, не допускает null ключей и значений.
- Hashtable - устаревшая синхронизированная карта; обычно заменяется ConcurrentHashMap или Collections.synchronizedMap.
- EnumMap - очень быстрая карта для ключей-перечислений (enum); ограничена типом ключа.
- IdentityHashMap - сравнение ключей по ссылке (==) вместо equals; применяется в специализированных задачах.
Выбор зависит от требований: порядок - LinkedHashMap, сортировка - TreeMap, многопоточность - ConcurrentHashMap, экономия памяти и скорость с enum - EnumMap.
Аналоги в других языках
- JavaScript: Map и Object. Map сохраняет порядок вставки, позволяет ключи любых типов.
// JS const m = new Map(); m.set('a', 1); console.log(m.get('a'));1
- Python: dict. Быстрый хеш-словарь, с Python 3.7 гарантирует порядок вставки.
# Python d = {'a': 1} print(d['a'])1
- PHP: ассоциативные массивы (array) и класс ArrayObject. Ключи допускают строки и целые числа; поведение и типы отличаются от строгой generics-системы Java.
// PHP $m = ['a' => 1]; var_dump($m['a']);int(1)
- C#: Dictionary<K,V> и Hashtable. Dictionary похож на HashMap, generics-типизация, не потокобезопасен.
// C# var d = new Dictionary<string,int>(); d["a"] = 1; Console.WriteLine(d["a"]);1
- Go: map[K]V. Простая синтаксическая конструкция, не потокобезопасна.
// Go m := map[string]int{"a":1} fmt.Println(m["a"])1
- Kotlin: MutableMap и HashMap. Практически соответствуют Java-реализациям, синтаксис короче.
val m = HashMap<String,Int>(); m["a"] = 1 println(m["a"])1
- Lua: table используется как ассоциативный массив.
-- Lua t = {a = 1} print(t.a)1
- SQL: реляционные таблицы используются для хранения пар ключ-значение, но доступ и индексация устроены иначе; не является прямым аналогом in-memory map.
Главные отличия: динамическая типизация и семантика null/undefined, гарантии порядка, поведение при многопоточности и особенности ключей (по ссылке или по значению).
Типичные ошибки и их проявления
- Неправильно реализованы equals и hashCode: ключи кажутся разными, хотя логически должны быть одинаковыми. Результат - дублирование записей и потеря поиска.
class Key { int id; Key(int id){this.id=id;} } HashMap<Key,String> m = new HashMap<>(); Key k1 = new Key(1); Key k2 = new Key(1); m.put(k1, "v1"); System.out.println(m.get(k2));null
Без переопределения equals/hashCode поиск по эквивалентному экземпляру вернёт null.
- Изменяемые поля ключа: если объект-ключ изменяется так, что его hashCode или equals меняются после вставки, элемент становится недоступным для поиска.
class K { int x; K(int x){this.x=x;} public int hashCode(){return x;} public boolean equals(Object o){return o instanceof K && ((K)o).x==x;} } K k = new K(1); HashMap<K,String> m2 = new HashMap<>(); m2.put(k, "val"); k.x = 2; System.out.println(m2.get(k));null
- ConcurrentModificationException: модификация карты в процессе итерации без использования итератора.remove() или корректной синхронизации.
HashMap<String,Integer> map = new HashMap<>(); map.put("a",1); for (String k : map.keySet()) { map.put("b",2); } System.out.println("done");Exception in thread "main" java.util.ConcurrentModificationException at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:...) - Ожидание порядка: использование HashMap для детерминированного порядка приводит к неожиданным результатам. Для порядка вставки лучше LinkedHashMap, для сортировки - TreeMap.
Изменения и важные улучшения в последних версиях
- В JDK 8 выполнена переработка внутреннего устройства HashMap: при превышении длины цепочки (по умолчанию 8) применяется преобразование корзины в сбалансированное дерево (red-black tree), что улучшает худший случай поиска с O(n) до O(log n).
- В JDK 8 в интерфейс Map добавлены методы с реализациями по умолчанию:
getOrDefault,putIfAbsent,computeIfAbsent,computeIfPresent,compute,merge,forEach,replaceAll. Это расширило возможности работы с картой без внешних обёрток. - Оптимизации рехешинга и работы с памятью в последующих релизах JDK, направленные на уменьшение затрат на аллокацию и улучшение производительности при больших объёмах данных.
Расширенные и нестандартные примеры применения
Несколько практических шаблонов и приёмов.
1) Счётчик слов с использованием merge
HashMap<String,Integer> freq = new HashMap<>();
String[] words = {"a","b","a","c","a"};
for (String w : words) {
freq.merge(w, 1, Integer::sum);
}
System.out.println(freq);
{a=3, b=1, c=1}
2) Multimap-паттерн через computeIfAbsent
HashMap<String, List<Integer>> mm = new HashMap<>();
mm.computeIfAbsent("k", k -> new ArrayList<>()).add(1);
mm.computeIfAbsent("k", k -> new ArrayList<>()).add(2);
System.out.println(mm);
{k=[1, 2]}
3) Предотвращение лишних рехешей - предварительная настройка initialCapacity
int expected = 1_000_000;
HashMap<String,String> large = new HashMap<>((int)(expected / 0.75f) + 1);
System.out.println(large.size());
0
Пояснение: расчёт ёмкости уменьшает число перераспределений при заполнении.
4) Использование streams для фильтрации и сбора
HashMap<String,Integer> map = new HashMap<>();
map.put("a", 1); map.put("b", 2); map.put("c", 3);
Map<String,Integer> filtered = map.entrySet().stream()
.filter(e -> e.getValue() % 2 == 1)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
System.out.println(filtered);
{a=1, c=3}
5) Сериализация и клонирование
HashMap<String,String> src = new HashMap<>();
src.put("x","y");
HashMap<String,String> copy = (HashMap<String,String>) src.clone();
System.out.println(copy);
{x=y}
Клонирование поверхностное: ссылки на ключи/значения не копируются.
6) Получение безопасного вида для многопоточной среды
Map<String,Integer> sync = Collections.synchronizedMap(new HashMap<>());
sync.put("a",1);
System.out.println(sync.get("a"));
1
Пояснение: при итерации требуется синхронизация вручную на объекте sync.
7) Работа с пользовательским ключом: корректная реализация equals/hashCode
class User {
final String id;
User(String id){this.id=id;}
public boolean equals(Object o){
return o instanceof User && id.equals(((User)o).id);
}
public int hashCode(){
return id.hashCode();
}
}
HashMap<User,String> hm = new HashMap<>();
User u1 = new User("42");
hm.put(u1, "data");
User u2 = new User("42");
System.out.println(hm.get(u2));
data
8) Удаление через views: keySet().removeIf
HashMap<String,Integer> m = new HashMap<>();
m.put("a",1); m.put("b",2);
m.keySet().removeIf(k -> k.equals("a"));
System.out.println(m);
{b=2}
9) Использование compute для атомарных пересчётов значений
HashMap<String,Integer> stats = new HashMap<>();
stats.compute("x", (k,v) -> v == null ? 1 : v + 1);
System.out.println(stats);
{x=1}
10) Пример ловушки: изменение структуры карты внутри forEach вызывает ConcurrentModificationException в старых обходах; методы compute/merge безопаснее для атомарных обновлений в однопоточной логике.