HashMap: примеры (JAVA)

HashMap: примеры кода и объяснения
Раздел: Коллекции (Collection Framework) - Map
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 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 function) - применяет функцию ко всем записям, заменяя значения.
    • V computeIfAbsent(K key, Function mappingFunction) - если ключа нет, вычисляет и сохраняет значение; возвращает текущее значение.
    • V computeIfPresent(K key, BiFunction remappingFunction) - при наличии ключа пересчитывает значение; если функция возвращает null, запись удаляется.
    • V compute(K key, BiFunction remappingFunction) - универсальная операция пересчёта; поведение аналогично: если возвращается null, запись удаляется.
    • V merge(K key, V value, BiFunction 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

Пример java
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

Пример java
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

Пример java
int expected = 1_000_000;
HashMap<String,String> large = new HashMap<>((int)(expected / 0.75f) + 1);
System.out.println(large.size());
0

Пояснение: расчёт ёмкости уменьшает число перераспределений при заполнении.

4) Использование streams для фильтрации и сбора

Пример java
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) Сериализация и клонирование

Пример java
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) Получение безопасного вида для многопоточной среды

Пример java
Map<String,Integer> sync = Collections.synchronizedMap(new HashMap<>());
sync.put("a",1);
System.out.println(sync.get("a"));
1

Пояснение: при итерации требуется синхронизация вручную на объекте sync.

7) Работа с пользовательским ключом: корректная реализация equals/hashCode

Пример java
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

Пример java
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 для атомарных пересчётов значений

Пример java
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 безопаснее для атомарных обновлений в однопоточной логике.

джава HashMap function comments

En
HashMap Hash table based implementation of the Map interface