Урок 33. Коллекция Map – Devcolibri

Урок 33. Коллекция Map

После просмотра видеоверсии урока обязательно изучите текстовый материал. Он дополняет видеоматериал и позволит вам полностью понять тему урока.

Итерация по элементам

Коллекция Map хранит элементы в формате [key, value]. Один такой объект называется Entry. Соответственно, у него есть методы getKey, getValue. Поэтому итерацию по элементам коллекции можно делать, используя метод entrySet:

public class Main {

    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();

        map.put("3", "Михаил");
        map.put("2", "Пётр");
        map.put("1", "Иван");

        for (Map.Entry<String, String> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " : " +entry.getValue());
        }
    }
}

Результат:

1 : Иван
2 : Пётр
3 : Михаил

HashMap не хранит порядок вставки элементов

Реализация интерфейса MapHashMap не хранит элементы в том же порядке, в каком вы их добавляете. В предыдущем примере результат был такой:

1 : Иван
2 : Пётр
3 : Михаил

Хотя мы ожидали увидеть вот такой:

3 : Михаил
2 : Пётр
1 : Иван

Если вам необходимо сохранять порядок элементов, то нужно использовать реализацию LinkedHashMap:

public class Main {

    public static void main(String[] args) {
        Map<String, String> map = new LinkedHashMap<>();

        map.put("3", "Михаил");
        map.put("2", "Пётр");
        map.put("1", "Иван");

        for (Map.Entry<String, String> entry : map.entrySet()) {
            System.out.println(entry.getKey() + " : " +entry.getValue());
        }
    }
}

Результат:

3 : Михаил
2 : Пётр
1 : Иван

В этом и есть основное их отличие. Если вам интересно детальнее рассмотреть, как работают эти классы «под капотом», то рекомендуем посмотреть статьи про HashMap, LinkedHashMap.

Как происходит добавление элементов в HashMap

Давайте рассмотрим алгоритм добавления элемента в HashMap:

map.put(person, 1);
  1. Вначале генерируется хэш на основе ключа. Для генерации используется метод hash(hashCode), в который передается person.hashCode().
static int hash(int h)
{
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
  1. С помощью метода indexFor(hash, tableLength) определяется позиция в массиве, куда будет помещен элемент.
static int indexFor(int h, int length)
{
    return h & (length - 1);
}
  1. Теперь, зная индекс в массиве, мы получаем список (цепочку) элементов, привязанных к этой ячейке. Хэш и ключ нового элемента поочередно сравниваются с хэшами и ключами элементов из списка, и при совпадении этих параметров значение элемента перезаписывается. Именно здесь мы можем увидеть использование метода equals:
if (e.hash == hash && (e.key == key || key.equals(e.key)))
{
    V oldValue = e.value;
    e.value = value;
                
    return oldValue;
}
  1. Если же предыдущий шаг не выявил совпадений, будет вызван метод addEntry(hash, key, value, index) для добавления нового элемента.

Извлечение элементов из HashMap происходит аналогично:

map.get(person);
  • Вычисляется hash, используя вызов person.hashcode().
  • Вычисляется позиция в массиве.
  • Сравниваются все элементы цепочки, используя вызов person.equals().
  • Если найден элемент object.equals(person) == true, то возвращается значение value.
  • Если не найден, то возвращается значение null.

Обязательно переопределяйте методы equals, hashcode у объектов-ключей в HashMap

Как вы увидели, HashMap использует методы equals, hashcode для добавления и извлечения элементов. Поэтому необходимо у объектов, которые используются в HashMap в качестве ключей, переопределять эти методы. Рассмотрим ситуацию на примере:

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
public class Main {
    public static void main(String[] args) {
        Person person = new Person("Human", 34);

        Map<Person, Integer> map = new HashMap<>();
        map.put(person, 1);

        Person samePerson = new Person("Human", 34);

        System.out.println(map.get(samePerson));
    }
}

Результат:

null

В реальной жизни мы не всегда работаем с одним и тем же объектом. Поэтому мы создали объект samePerson с такими же данными (имя и возраст остались прежними). Это может произойти, когда мы получаем объект c теми же самыми данными от сервера или от базы данных. После того, как мы это сделали HashMap, не смог найти значение по этому ключу. Это случилось, потому что по умолчанию методы equals и hashcode реализованы в классе Object следующим образом:

public class Object {
    
    public native int hashCode();

    public boolean equals(Object var1) {
        return this == var1;
    }
}

По умолчанию Object.equals() сравнивает ссылки, а не содержимое объектов. Перейдём к методу hashcode(). Ключевое слово native означает, что реализация данного метода выполнена на другом языке, например на C, C++ или ассемблере (в данном случае метод реализован на C++). Метод написан так, что при каждом запуске программы у объекта будет разный хэш-код. Т.е. у двух объектов с одинаковым содержимым hashcode по умолчанию будет разным. Пример:

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Human", 34);
        Person samePerson = new Person("Human", 34);

        System.out.println(person.equals(samePerson));
        System.out.println(person.hashCode());
        System.out.println(samePerson.hashCode());
    }
}

Результат:

false
1580066828
491044090

Функция equals вернула значение false, и значения функций hashcode у двух объектов тоже разные. Теперь вы понимаете, почему при такой реализации HashMap не может найти элемент (если нет, то вернитесь сюда и посмотрите алгоритм добавления элементов ещё раз). Чтобы исправить код, необходимо сгенерировать у класса Person методы equals и hashcode. Сделать это можно, нажав Alt+Insert->equals() and hashcode() и выбрав все поля класса. После этого класс выглядит так:

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {

        return Objects.hash(name, age);
    }
}

Можете не вникать детально в суть работы этих методов. Просто запомните, что теперь мы переопределили методы equals, hashcode класса Object, и при вызове этих методов будет сравниваться содержимое объектов. Запустим код ещё раз:

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Human", 34);
        Person samePerson = new Person("Human", 34);

        System.out.println(person.equals(samePerson));
        System.out.println(person.hashCode());
        System.out.println(samePerson.hashCode());
    }
}

Результат:

true
-2122271626
-2122271626

Запустим ещё раз код, который работал с HashMap:

public class Main {
    public static void main(String[] args) {
        Person person = new Person("Human", 34);

        Map<Person, Integer> map = new HashMap<>();
        map.put(person, 1);

        Person samePerson = new Person("Human", 34);

        System.out.println(map.get(samePerson));
    }
}

Результат:

1

Теперь вы понимаете, зачем важно переопределять методы equals, hashcode при работе с коллекцией HashMap. Полезные материалы:

Возникли проблемы при прохождении? Напишите нам в чат поддержки Вконтакте или Facebook. Мы поможем вам решить проблему и вы сможете продолжить обучение.
УВИДЕТЬ ВСЕ Добавить заметку
ВЫ
Добавить ваш комментарий