Плохая идея использовать строковый ключ в HashMap? - PullRequest
66 голосов
/ 04 октября 2009

Я понимаю, что метод класса String ' hashCode () является , а не гарантированным для генерации уникальных хеш-кодов для различных String-s. Я вижу много случаев использования ключей String в HashMap-s (используя метод String hashCode () по умолчанию). Большая часть этого использования может привести к значительным проблемам приложения, если карта put сместит запись HashMap, которая ранее была помещена на карту с действительно отличным ключом String.

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

Ответы [ 5 ]

112 голосов
/ 04 октября 2009

Разработчикам не нужно обходить проблему коллизий хешей в HashMap, чтобы достичь корректности программы.

Здесь нужно понять пару ключевых моментов:

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

  2. Каждое использование хеширования имеет способ обрабатывать коллизии, и коллекции Java (включая HashMap) не являются исключением.

  3. Хеширование не участвует в тестировании на равенство. Это правда, что равные объекты должны иметь одинаковые хеш-коды, но обратное неверно: многие значения будут иметь одинаковый хеш-код. Поэтому не пытайтесь использовать сравнение хеш-кода в качестве замены равенства. Коллекции нет. Они используют хеширование, чтобы выбрать подколлекцию (называемую корзиной в мире коллекций Java), но они используют .equals (), чтобы фактически проверить на равенство.

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

  5. Еще лучше: для случая, о котором вы спрашивали (строки как ключи), вам даже не нужно беспокоиться о самих хеш-кодах, потому что класс String в Java генерирует довольно хороший хеш-код. Как и большинство поставляемых классов Java.

Еще немного подробностей, если хотите:

Способ хэширования (в частности, в случае хэшированных коллекций, таких как Java HashMap, о чем вы спрашивали), таков:

  • HashMap хранит значения, которые вы ему даете, в коллекции вложенных коллекций, называемых сегментами. Они фактически реализованы в виде связанных списков. Их число ограничено: iirc, 16 запускается по умолчанию, и это число увеличивается с увеличением количества элементов на карте. Всегда должно быть больше сегментов, чем значений. Чтобы привести один пример, используя значения по умолчанию, если вы добавите 100 записей в HashMap, будет 256 блоков.

  • Каждое значение, которое можно использовать в качестве ключа на карте, должно иметь возможность генерировать целочисленное значение, называемое хеш-кодом.

  • HashMap использует этот хэш-код для выбора сегмента. В конечном итоге это означает принятие целочисленного значения modulo количества сегментов, но до этого в HashMap в Java есть внутренний метод (называемый hash()), который настраивает хэш-код для уменьшения некоторых известных источников объединения.

  • При поиске значения HashMap выбирает сегмент, а затем ищет отдельный элемент путем линейного поиска по связанному списку, используя .equals().

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

В случае, когда вам действительно нужно написать свой собственный метод хэш-кода (что означает, что вы написали класс с составным значением, таким как пара имя / фамилия), все становится немного сложнее. Здесь вполне возможно ошибиться, но это не ракетостроение. Во-первых, знайте это: единственное, что вы должны сделать, чтобы убедиться в правильности, - это убедиться, что равные объекты дают одинаковые хеш-коды. Поэтому, если вы пишете метод hashcode () для своего класса, вы также должны написать метод equals (), и вы должны проверить одинаковые значения в каждом из них.

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

Каноническим вырожденным наихудшим случаем этого было бы написание метода, который просто возвращает постоянное значение (например, 3) для всех случаев. Это будет означать, что каждое значение будет хэшировано в одно и то же ведро.

Это все равно будет работать , но производительность будет снижаться по сравнению со связным списком.

Очевидно, вы не напишите такой ужасный метод hashcode (). Если вы используете достойную IDE, она способна сгенерировать ее для вас. Так как StackOverflow любит код, вот код для класса имени / фамилии выше.


public class SimpleName {
    private String firstName;
    private String lastName;
    public SimpleName(String firstName, String lastName) {
        super();
        this.firstName = firstName;
        this.lastName = lastName;
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result
                + ((firstName == null) ? 0 : firstName.hashCode());
        result = prime * result
                + ((lastName == null) ? 0 : lastName.hashCode());
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        SimpleName other = (SimpleName) obj;
        if (firstName == null) {
            if (other.firstName != null)
                return false;
        } else if (!firstName.equals(other.firstName))
            return false;
        if (lastName == null) {
            if (other.lastName != null)
                return false;
        } else if (!lastName.equals(other.lastName))
            return false;
        return true;
    }
}

4 голосов
/ 03 июня 2013

Я направляю вас к ответу здесь . Хотя не является плохой идеей использовать строки (@CPerkins объяснил почему, совершенно), сохранение значений в хеш-карте с целочисленными ключами является лучше , поскольку это обычно быстрее (хотя и незаметно) и имеет меньшую вероятность (фактически, нет шансов) столкновений.

См. Этот график столкновений с использованием 216553 ключей в каждом случае (украдено из этого поста , переформатировано для нашего обсуждения)

Hash           Lowercase      Random UUID  Numbers 
=============  =============  ===========  ==============
Murmur            145 ns      259 ns          92 ns
                    6 collis    5 collis       0 collis
FNV-1a            152 ns      504 ns          86 ns
                    4 collis    4 collis       0 collis
FNV-1             184 ns      730 ns          92 ns
                    1 collis    5 collis       0 collis*
DBJ2a             158 ns      443 ns          91 ns
                    5 collis    6 collis       0 collis***
DJB2              156 ns      437 ns          93 ns
                    7 collis    6 collis       0 collis***
SDBM              148 ns      484 ns          90 ns
                    4 collis    6 collis       0 collis**
CRC32             250 ns      946 ns         130 ns
                    2 collis    0 collis       0 collis

Avg Time per key    0.8ps       2.5ps         0.44ps
Collisions (%)      0.002%      0.002%         0%

Конечно, число целых чисел ограничено 2 ^ 32, где нет ограничения на количество строк (и нет теоретического ограничения на количество ключей, которые можно сохранить в HashMap) , Если вы используете long (или даже float), столкновения будут неизбежны, и, следовательно, не "лучше", чем строка. Однако, даже несмотря на коллизии хешей, put() и get() всегда будут выставлять / получать правильную пару ключ-значение (см. Правку ниже).

В конце концов, это действительно не имеет значения, поэтому используйте все, что удобнее. Но если удобство не имеет значения, и вы не собираетесь иметь более 2 ^ 32 записей, я предлагаю вам использовать ints в качестве ключей.


EDIT

Хотя приведенное выше определенно верно, НИКОГДА не используйте "StringKey" .hashCode () для генерации ключа вместо исходного ключа String по соображениям производительности - 2 разные строки могут иметь одинаковый hashCode, что приводит к перезаписи на вашем put() метод. Реализация Java HashMap достаточно умна, чтобы обрабатывать строки (фактически, любой тип ключа) с одним и тем же хеш-кодом автоматически, поэтому разумно позволить Java обработать эти вещи для вас.

4 голосов
/ 04 октября 2009

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

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

Как правило, это работает очень хорошо, но только в том случае, если хеш-функция хороша, то есть не приводит к статистически ожидаемому количеству коллизий для вашего конкретного входного набора. String.hashCode() хорошо в этом отношении, но так было не всегда. Предположительно , до Java 1.2 он включал только каждый n-й символ. Это было быстрее, но вызывало предсказуемые коллизии для всех String, разделяющих каждый n-й символ - очень плохо, если вам не повезло, чтобы иметь такой регулярный ввод, или если кто-то хочет провести DOS-атаку на ваше приложение.

4 голосов
/ 04 октября 2009

Я сильно подозреваю, что метод HashMap.put не определяет, является ли ключ одинаковым, просто взглянув на String.hashCode.

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

Следовательно, новый ключ String будет оцениваться как тот же ключ String, что и ключ HashMap, только если значение, возвращаемое hashCode равно, и метод equals возвращает true.

Также добавим, что эта мысль была бы верна и для классов, отличных от String, так как сам класс Object уже имеет hashCode и equals методы.

Редактировать

Итак, чтобы ответить на вопрос, нет, было бы неплохо использовать String для ключа к HashMap.

2 голосов
/ 04 октября 2009

Вы говорите о хеш-коллизиях. Хеш-коллизии являются проблемой независимо от типа hashCode'd. Все классы, которые используют hashCode (например, HashMap), обрабатывают коллизии хеша просто отлично. Например, HashMap может хранить несколько объектов на одну корзину.

Не беспокойтесь, если вы сами не вызываете hashCode. Хеш-коллизии, хотя и редкие, ничего не ломают.

...