Почему hashCode () может возвращать одно и то же значение для разных объектов в Java? - PullRequest
16 голосов
/ 05 декабря 2010

Цитата из книги, которую я читаю Head First Java :

Дело в том, что хеш-коды могут быть одинаковыми, не обязательно гарантируя, что объекты равны, поскольку «алгоритм хеширования», используемый в методе hashCode(), может возвращать одно и то же значение для нескольких объектов.

Почему метод hashCode() может возвращать одно и то же значение для разных объектов? Разве это не вызывает проблем?

Ответы [ 6 ]

31 голосов
/ 05 декабря 2010

хэширование объект означает " поиск хорошего описательного значения (числа), которое может быть воспроизведено одним и тем же экземпляром снова и снова ".Поскольку хеш-коды из Object.hashCode() Java имеют тип int, вы можете иметь только 2^32 различных значений.Вот почему у вас будут так называемые «коллизии» в зависимости от алгоритма хеширования, когда два разных объекта выдают одинаковый hashCode.

Как правило, это не создает никаких проблем, потому что hashCode() в основном используется вместе сequals().Например, HashMap вызовет hashCode() для своих ключей, чтобы узнать, могут ли ключи уже содержаться в HashMap.Если HashMap не находит хеш-код, очевидно, что ключ еще не содержится в HashMap.Но если это произойдет, ему придется перепроверить все ключи, имеющие тот же хэш-код, используя equals().

Т.е.

A.hashCode() == B.hashCode() // does not necessarily mean
A.equals(B)

Но

A.equals(B) // means
A.hashCode() == B.hashCode()

Если equals() и hashCode() реализованы правильно.

Более точное описание общего контракта hashCode см. В Javadoc .

.
26 голосов
/ 05 декабря 2010

Существует всего чуть более 4 миллиардов возможных хеш-кодов (диапазон int), но количество объектов, которые вы можете выбрать, гораздо больше.Поэтому некоторые объекты должны использовать один и тот же хэш-код по принципу pigeonhole .

Например, число возможных строк, содержащих 10 букв из AZ, равно 26 ** 10, что равно 141167095653376невозможно присвоить всем этим строкам уникальный хеш-код.И это не важно - хеш-код не должен быть уникальным.Просто нужно иметь не слишком много коллизий для реальных данных.

16 голосов
/ 05 декабря 2010

Идея хеш-таблицы заключается в том, что вы хотите эффективно реализовать структуру данных, называемую словарем.Словарь - это хранилище ключей / значений, т. Е. Вы хотите иметь возможность хранить определенные объекты под определенным ключом, а затем снова получать их, используя тот же ключ.

Один из наиболее эффективных способовДоступ к значениям - это сохранение их в массиве.Например, мы могли бы реализовать словарь, который использует целые числа для ключей и строки для значений, например:

String[] dictionary = new String[DICT_SIZE];
dictionary[15] = "Hello";
dictionary[121] = "world";

System.out.println(dictionary[15]); // prints "Hello"

К сожалению, этот подход не очень общий: индекс массива должен быть целым числомзначение, но в идеале мы хотели бы иметь возможность использовать произвольные виды объектов для наших ключей, а не только целые числа.

Теперь, чтобы решить эту проблему, нужно отобразить произвольные объекты в целочисленные значения, которые мы могли бы затем использовать в качестве ключей для нашего массива.В Java это то, что делает hashCode().Итак, теперь мы можем попытаться реализовать словарь String-> String:

String[] dictionary = new String[DICT_SIZE];
// "a" -> "Hello"
dictionary["a".hashCode()] = "Hello";

// "b" -> "world"
dictionary["b".hashCode()] = "world";

System.out.println(dictionary["b".hashCode()]); // prints world

Но, эй, что если есть какой-то объект, который мы хотели бы использовать в качестве ключа, но его метод hashCodeвозвращает значение, которое больше или равно DICT_SIZE?Тогда мы получили бы ArrayIndexOutOfBoundsException, и это было бы нежелательно.Итак, давайте просто сделаем его настолько большим, насколько сможем, верно?

public static final int DICT_SIZE = Integer.MAX_VALUE // Ooops!

Но это будет означать, что нам придется выделять огромные объемы памяти для нашего массива, даже если мы собираемся хранить только несколькоПредметы.Так что это не может быть лучшим решением, и на самом деле мы можем добиться большего.Предположим, у нас была функция h, которая для любого заданного DICT_SIZE отображает произвольные целые числа в диапазон [0, DICT_SIZE[.Тогда мы могли бы просто применить h к любому возвращаемому методу hashCode() ключевого объекта и быть уверенным, что мы остаемся в границах базового массива.

public static int h(int value, int DICT_SIZE) {
    // returns an integer >= 0 and < DICT_SIZE for every value.
}

Эта функция называется хеш-функцией,Теперь мы можем адаптировать нашу реализацию словаря, чтобы исключить ArrayIndexOutOfBoundsException:

// "a" -> "Hello"
dictionary[h("a".hashCode(), DICT_SIZE)] = "Hello"

// "b" -> "world"
dictionary[h("b".hashCode(), DICT_SIZE)] = "world"

Но это создает другую проблему: что если h отображает два разных ключевых индекса на одно и то же значение?Например:

int keyA = h("a".hashCode(), DICT_SIZE);
int keyB = h("b".hashCode(), DICT_SIZE);

может дать одинаковые значения для keyA и keyB, и в этом случае мы случайно перезаписываем значение в нашем массиве:

// "a" -> "Hello"
dictionary[keyA] = "Hello";

// "b" -> "world"
dictionary[keyB] = "world"; // DAMN! This overwrites "Hello"!!

System.out.println(dictionary[keyA]); // prints "world"

Хорошо, вы можете сказать, тогда мы просто должны убедиться, что мы реализуем h таким образом, что этого никогда не произойдет.К сожалению, это невозможно вообще.Рассмотрим следующий код:

for (int i = 0; i <= DICT_SIZE; i++) {
    dictionary[h(i, DICT_SIZE)] = "dummy";
}

Этот цикл хранит значения DICT_SIZE + 1 (на самом деле всегда одно и то же, а именно String "dummy") в словаре.Ммм, но массив может хранить только DICT_SIZE разных записей!Это означает, что когда мы используем h, мы перезаписываем (как минимум) одну запись.Или, другими словами, h отобразит два разных ключа на одно и то же значение!Эти «столкновения» не могут быть предотвращены: если n голубей пытаются проникнуть в n-1 голубиных отверстий, по крайней мере два из них должны войти в одну и ту же дыру.

Но мы можем расширитьнаша реализация, так что массив может хранить несколько значений под одним и тем же индексом.Это легко сделать с помощью списков.Поэтому вместо использования:

String[] dictionary = new String[DICT_SIZE];

мы пишем:

List<String>[] dictionary = new List<String>[DICT_SIZE];

(Примечание: обратите внимание, что Java не позволяет создавать массивы универсальных типов, поэтому приведенная выше строка будетне компилировать - но вы поняли).

Это изменит доступ к словарю следующим образом:

// "a" -> "Hello"
dictionary[h("a".hashCode(), DICT_SIZE)].add("Hello");

// "b" -> "world"
dictionary[h("b".hashCode(), DICT_SIZE)].add("world");

В случае, если наша хэш-функция h возвращает разные значения для всехнаши ключи, в результате мы получим списки только с одним элементом, и получение элементов действительно просто:

System.out.println(dictionary[h("a".hashCode(), DICT_SIZE)].get(0)); // "Hello"

Но мы уже знаем, что в общем случае h иногда отображает разные ключи в одно и то же целое число.В этих случаях списки будут содержать более одного значения.Для поиска нам нужно пройти через весь список, чтобы найти «правильное» значение, но как бы мы его распознали?

Ну, вместо того, чтобы хранить только одно значение, мы всегда могли бы сохранить полное (ключ,значение) пара в списках.Тогда поиск будет выполняться в два этапа:

  1. Применение хэш-функции для получения правильного списка из массива.
  2. Итерация по всем парам, сохраненным в найденном списке: если пара с нужным ключом найдена, вернуть значение из пары.

Теперь добавление и извлечение стали настолько сложными, что весьма неплохо рассматривать отдельные методы для этих операций:

List<Pair<String,String>>[] dictionary = List<Pair<String,String>>[DICT_SIZE];

public void put(String key, String value) {
    int hashCode = key.hashCode();
    int arrayIndex = h(hashCode, DICT_SIZE);

    List<Pair<String,String>> listAtIndex = dictionary[arrayIndex];
    if (listAtIndex == null) {
        listAtIndex = new LinkedList<Pair<Integer,String>>();
        dictionary[arrayIndex] = listAtIndex;
    }

    for (Pair<String,String> previouslyAdded : listAtIndex) {
        if (previouslyAdded.getValue().equals(value)) {
            return; // the value is already in the dictionary;
        }
    }

    listAtIndex.add(new Pair<String,String>(key, value));
}

public String get(String key) {
    int hashCode = key.hashCode();
    int arrayIndex = h(hashCode, DICT_SIZE);

    List<Pair<String,String>> listAtIndex = dictionary[arrayIndex];
    if (listAtIndex != null) {
        for (Pair<String,String> previouslyAdded : listAtIndex) {
            if (previouslyAdded.getKey().equals(key)) {
                return previouslyAdded.getValue(); // entry found!
            }
        }
    }

    // entry not found
    return null;
}

Итак, чтобы этот подход работал,нам на самом деле нужны две операции сравнения: метод hashCode, чтобы найти список в массиве (это работает быстро, если hashCode() и h оба являются быстрыми) и метод equals, который нам нужен при просмотре списка.

Это общая идея хеширования, и вы узнаете метод put и get из java.util.Map.. Конечно, приведенная выше реализация является упрощением, но она должна иллюстрировать суть всего этого.

Естественно, этот подход не ограничивается строками, он работает для всех видов объектов, поскольку методы hashCode() и equals arЧлены класса верхнего уровня java.lang.Object и все другие классы наследуются от него.

Как видите, на самом деле не имеет значения, возвращают ли два разных объекта одинаковое значение в их hashCode() метод: вышеуказанный подход всегда будет работать!Но все же желательно, чтобы они возвращали разные значения, чтобы снизить вероятность коллизий хешей, вызванных h.Мы видели, что этого нельзя избежать на 100% в целом, но чем меньше мы получаем коллизий, тем эффективнее становится наша хеш-таблица.В худшем случае все ключи отображаются на один и тот же индекс массива: в этом случае все пары хранятся в одном списке, и поиск значения становится операцией с линейными затратами в размере хеш-таблицы.

2 голосов
/ 05 декабря 2010

Значение hashCode () можно использовать для быстрого поиска объекта, используя хеш-код в качестве адреса корзины хеш-таблицы, где он хранится.

Если несколько объектов возвращают одно и то же значение из hashCode (), это означает, что они будут храниться в одном сегменте. Если в одном сегменте хранится много объектов, это означает, что в среднем требуется больше операций сравнения для поиска данного объекта.

Вместо этого используйте equals () для сравнения двух объектов, чтобы увидеть, являются ли они семантически равными.

0 голосов
/ 05 декабря 2010

Мне нужно подумать, что это довольно неэффективный алгоритм хеширования для двух объектов с одинаковым хеш-кодом.

0 голосов
/ 05 декабря 2010

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

...