Плохая идея использовать строковый ключ в HashMap?



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



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

980   5  

5 ответов:

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

есть несколько ключевых вещей, чтобы понять здесь:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

все равно работа, но производительность снизится до уровня связанного списка.

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


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;
    }
}

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

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

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

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

Edit

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

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

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

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

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

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

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 разные строки могут иметь один и тот же хэш-код, вызывая перезапись на вашем put() метод. Реализация Java HashMap достаточно умен, чтобы обрабатывать строки (любой тип ключа, на самом деле) с тем же хэш-кодом автоматически, поэтому разумно позволить Java обрабатывать эти вещи для вас.

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

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

Comments

    Ничего не найдено.