Введение: 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