HashMap в Java: 5 фатальных ошибок при обходе и как их избежать

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: HashMap — друг или враг?

Каждый Java-разработчик рано или поздно сталкивается с HashMap. Это, пожалуй, самая популярная реализация интерфейса Map в Java. Но вот парадокс: все знают, как положить элемент в карту, но когда доходит до её обхода (итерации), начинается настоящий хаос. Вопрос "Iterate through a HashMap" на Stack Overflow набрал тысячи просмотров, а количество ошибок в реальных проектах зашкаливает.

В этой статье мы разберём 5 самых опасных ошибок при обходе HashMap на Java, которые могут привести к падению приложения, потере данных и нечитаемому коду. Вы узнаете не только как неправильно, но и получите рабочие шаблоны для каждого случая. Поехали!

Ошибка №1: ConcurrentModificationException — "Удаляю на ходу"

В чём проблема?

Самая классическая ошибка. Вы итерируетесь по HashMap и пытаетесь удалить элемент через map.remove(key). В 90% случаев это приводит к ConcurrentModificationException.

// ❌ ОПАСНО: вызовет исключение!
HashMap<String, Integer> scores = new HashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 50);
scores.put("Charlie", 70);

for (String name : scores.keySet()) {
    if (scores.get(name) < 60) {
        scores.remove(name); // ConcurrentModificationException!
    }
}

Почему так происходит?

Внутренний итератор HashMap ведёт счётчик модификаций (modCount). Когда вы удаляете элемент напрямую через коллекцию, счётчик меняется, но итератор об этом не знает. При следующей итерации он проверяет: "Эй, счётчик не совпадает! Бросаем исключение".

Как исправить?

Используйте Iterator с его методом remove():

// ✅ ПРАВИЛЬНО: используем Iterator
HashMap<String, Integer> scores = new HashMap<>();
scores.put("Alice", 90);
scores.put("Bob", 50);
scores.put("Charlie", 70);

Iterator<Map.Entry<String, Integer>> iterator = scores.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<String, Integer> entry = iterator.next();
    if (entry.getValue() < 60) {
        iterator.remove(); // Безопасное удаление!
    }
}

С Java 8 появился ещё более элегантный способ — removeIf():

// ✅ ЕЩЁ ЛУЧШЕ: removeIf (Java 8+)
scores.entrySet().removeIf(entry -> entry.getValue() < 60);

Ошибка №2: Медленный обход через keySet() и get()

В чём проблема?

Многие новички делают так:

// ❌ МЕДЛЕННО: keySet() + get() — двойной поиск
for (String key : map.keySet()) {
    System.out.println("Key: " + key + ", Value: " + map.get(key));
}

На первый взгляд всё работает. Но проблема в том, что map.get(key) внутри снова ищет хэш и проходит по бакету. При каждом обороте цикла вы делаете два вызова к HashMap вместо одного. Для маленьких карт это незаметно, но при сотнях тысяч записей — это потеря производительности до 30-40%.

Как исправить?

Всегда используйте entrySet() — он возвращает пары "ключ-значение" за один проход:

// ✅ БЫСТРО: entrySet() — один проход
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}

Или используйте forEach с Java 8:

// ✅ ЛАКОНИЧНО: forEach (Java 8+)
map.forEach((key, value) -> {
    System.out.println("Key: " + key + ", Value: " + value);
});

Ошибка №3: Путаница с порядком элементов

В чём проблема?

Вы написали код, который обходит HashMap, и ожидаете, что элементы будут в порядке добавления. Но получаете совершенно другую последовательность. Это приводит к багам, которые сложно отловить.

HashMap<String, String> map = new HashMap<>();
map.put("A", "Первый");
map.put("B", "Второй");
map.put("C", "Третий");

// ❌ НЕГАРАНТИРОВАННЫЙ ПОРЯДОК
for (String key : map.keySet()) {
    System.out.print(key + " "); // Может вывести: B, A, C или C, B, A...
}

Почему?

HashMap не гарантирует порядок элементов. Он основан на хэш-таблицах, и порядок зависит от хэш-кода ключей и размера корзины. Если вам важен порядок — используйте LinkedHashMap (сохраняет порядок вставки) или TreeMap (сортирует по ключу).

Как исправить?

// ✅ ГАРАНТИРОВАННЫЙ ПОРЯДОК ВСТАВКИ
LinkedHashMap<String, String> linkedMap = new LinkedHashMap<>();
linkedMap.put("A", "Первый");
linkedMap.put("B", "Второй");
linkedMap.put("C", "Третий");

for (String key : linkedMap.keySet()) {
    System.out.print(key + " "); // Всегда: A, B, C
}

Ошибка №4: Изменение ключа во время итерации

В чём проблема?

Представьте: вы обходите HashMap и решаете изменить значение ключа. Или, что ещё хуже, — меняете сам объект, который является ключом (если он mutable). Это может привести к тому, что вы "потеряете" элемент в коллекции.

// ❌ ОПАСНО: изменение ключа во время итерации
HashMap<StringBuilder, String> map = new HashMap<>();
StringBuilder key = new StringBuilder("oldKey");
map.put(key, "value");

for (StringBuilder k : map.keySet()) {
    k.append("Modified"); // Хэш-код изменился!
}
// Элемент может быть недоступен по старому ключу

Почему?

HashMap использует хэш-код объекта для определения корзины. Если вы измените объект-ключ после вставки, его хэш-код изменится, но карта не пересчитает его позицию. Элемент "зависнет" в неправильной корзине, и вы не сможете его найти.

Как исправить?

Используйте иммутабельные ключи (например, String, Integer, или свои классы, где поля не меняются). Если нужно изменить ключ — удалите старую запись и вставьте новую:

// ✅ ПРАВИЛЬНО: удалить старый, вставить новый
String oldKey = "oldKey";
String newKey = "newKey";
String value = map.remove                

Блоги

Рекомендаций по книгам