HashMap
Вы сдаёте пальто в гардероб и получаете номерок. Номерок — это ключ, а пальто — значение, которое по нему выдают. По номерку гардеробщик вернёт ваше пальто, а не чьё-то другое.
В Java такой гардероб — словарь Map: коллекция пар «ключ — значение», в которой значение находят по ключу. Чаще всего берут класс HashMap:
import java.util.HashMap;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> ages = new HashMap<>();
ages.put("Anna", 30);
ages.put("Boris", 25);
ages.put("Anna", 31);
System.out.println(ages.get("Anna")); // 31
System.out.println(ages.size()); // 2
System.out.println(ages.get("Vera")); // null
}
}Давайте разберём программу по строкам:
- У
Map<String, Integer>два параметра типа: тип ключаStringи тип значенияInteger. put("Anna", 30)кладёт пару: ключ"Anna", значение30.- Второй
putс ключом"Anna"не добавляет новую пару, а заменяет значение: было30, стало31. get("Anna")возвращает значение по ключу —31.ages.size()возвращает2: каждый ключ хранится один раз.get("Vera")возвращаетnull: такого ключа в словаре нет.
Ключи в словаре не повторяются, а значения могут: у Анны и Бориса может оказаться один возраст.
getOrDefault и containsKey
Классическая задача — посчитать, сколько раз встречается каждое слово. Ключом будет слово, значением — счётчик:
String[] words = {"to", "be", "or", "not", "to", "be"};
Map<String, Integer> counts = new HashMap<>();
for (String w : words) {
counts.put(w, counts.getOrDefault(w, 0) + 1);
}
System.out.println(counts.get("to")); // 2
System.out.println(counts.get("not")); // 1
System.out.println(counts.containsKey("hi")); // falseРазберём, как это работает:
counts.getOrDefault(w, 0)возвращает значение по ключуw, а если ключа нет —0.putзаписывает счётчик на единицу больше. Новое слово получает1, а"to"при второй встрече —2.containsKey("hi")проверяет, есть ли ключ, и отвечаетfalse.
Весь словарь тоже можно напечатать: System.out.println(counts) выводит пары «ключ=значение» в фигурных скобках, например {not=1, be=2, or=1, to=2}. Порядок пар не гарантирован, а пустой словарь печатается как {}.
Как HashMap хранит пары
Как устроен HashMap — один из самых частых вопросов на собеседовании по Java. Ответ вы отчасти уже знаете: HashSet из прошлого урока хранит свои элементы как ключи HashMap. Внутри словаря — массив корзин, это крючки нашего гардероба. Вот как HashMap с ними работает:
hashCodeключа превращается в номер корзины: от0до числа корзин минус один.- Если в корзине уже есть пары, новая встаёт в конец цепочки.
getидёт по цепочке одной корзины: сравнивает хеш-коды, а равный ключ подтверждаетequals. - Когда пар становится больше трёх четвертей от числа корзин, таблица удваивается и раскладывает пары заново.
Словарь new HashMap<>() получает 16 корзин при первом put и растёт, когда пар становится больше 12. В виджете таблица меньше, new HashMap<>(4), поэтому порог — 3 пары. Ключи cat, dog, owl, fox и bee получают значения от 1 до 5, а потом get("owl") ищет сову. Следите за корзиной 1: туда попадут два ключа. А на четвёртом put таблица вырастет до 8 корзин.
1Map<String, Integer> m = new HashMap<>(4);2m.put("cat", 1);3m.put("dog", 2);4m.put("owl", 3);5m.put("fox", 4);6m.put("bee", 5);7m.get("owl");
new HashMap<>(4) ещё не создал таблицу: её создаст первый put — на 4 корзины, с порогом 3 (0,75 × 4). Нажмите кнопку.
Разберём, что произошло:
- Первый
putсоздал таблицу на 4 корзины, и ключcatпопал в корзину 3. - У
dogиowlхеш-коды разные, но номер корзины один — 1. Их пары встали в цепочку друг за другом. - Пара с ключом
foxстала четвёртой при пороге 3, и таблица выросла до 8 корзин. Пары разложились заново:dogиowlпереехали в корзину 5,fox— в 6,cat— в 7. - Ключ
beeпопал в свободную корзину 3. get("owl")пошёл сразу в корзину 5. Уdogхеш-код другой, иequalsне понадобился. Уowlхеш-код совпал,equalsподтвердил ключ, иgetвернул3.
Поэтому get и put быстрые: они смотрят одну корзину, а не все пары. И поэтому у HashMap нет порядка: пары лежат не в порядке добавления, а после роста таблицы перемещаются.
Здесь картинка с гардеробом неточна. Гардеробщик не перевешивает пальто, когда открывает второй зал. А HashMap при росте перекладывает все пары, и номер корзины у ключа может смениться.
Частая ошибка
Результат get положили в int:
int n = counts.get("hi"); // ✗Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is nullКлюча "hi" нет, и get вернул null. За присваиванием в int прячется распаковка из темы об обёртках — вызов intValue(), а у null его не вызвать. Если нужно число, берите getOrDefault("hi", 0), а наличие ключа проверяйте через containsKey.
Главное
HashMapхранит пары «ключ — значение»:putкладёт пару, а с уже известным ключом заменяет значение;getдля отсутствующего ключа возвращаетnull.getOrDefaultдаёт значение по умолчанию, аcontainsKeyпроверяет, есть ли ключ.hashCodeключа выбирает корзину, а в цепочке корзины ключ находят по хеш-коду иequals.- Когда пар больше трёх четвертей от числа корзин, таблица удваивается и пары раскладываются заново, поэтому порядка у
HashMapнет.