Подтвердить что ты не робот

Почему у String.hashCode() в Java много конфликтов?

Почему у String.hashcode() столько конфликтов?

Я читаю String.hashCode() в jdk1.6, ниже приведены коды

public int hashCode() {
    int h = hash;
    if (h == 0) {
        int off = offset;
        char val[] = value;
        int len = count;

        for (int i = 0; i < len; i++) {
            h = 31*h + val[off++];
        }
        hash = h;
    }
    return h;
}

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

Предположим, что у нас есть два символа, тогда, пока мы можем найти две строки, соответствующие ниже уравнения, тогда у нас будет тот же hashcode()

a * 31 +b = c * 31 +d

Нетрудно заключить, что (a-c) * 31 = d-b взять простой пример: a-c = 1 и d-b = 31; поэтому я написал ниже коды для простого теста

public void testHash() {
    System.out.println("A:" + (int)'A');
    System.out.println("B:" + (int)'B');
    System.out.println("a:" + (int)'a');

    System.out.println("Aa".hashCode() + "," + "BB".hashCode());
    System.out.println("Ba".hashCode() + "," + "CB".hashCode());
    System.out.println("Ca".hashCode() + "," + "DB".hashCode());
    System.out.println("Da".hashCode() + "," + "EB".hashCode());        
}

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

A:65 
B:66
a:97
2112,2112
2143,2143
2174,2174
2205,2205

еще хуже, предположим, что у нас есть 4 символа в строке, согласно алгоритму, предположим, что первые 2 символа производят a2, 2-й символ - b2; хэш-код будет a2 * 31^2 + b2 таким образом, с a2 и b2, равными между двумя строками, мы получим больше строк с конфликтом hashcode(). такими примерами являются "AaAa", "BBBB" и т.д.; то у нас будет 6 символов, 8 символов......

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

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

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

16802,17028
17059,17285
17316,17542
17573,17799

но почему jdk не исправляет его? спасибо.

@Test
public void testBetterhash() {
    System.out.println(betterHash("Aa") + "," + betterHash("BB"));      
    System.out.println(betterHash("Ba") + "," + betterHash("CB"));
    System.out.println(betterHash("Ca") + "," + betterHash("DB"));
    System.out.println(betterHash("Da") + "," + betterHash("EB"));
}

public static int betterHash(String s) {
    int h = 0;
    int len = s.length();

    for (int i = 0; i < len; i++) {
        h = 257*h + s.charAt(i);
    }
    return h;
}
4b9b3361

Ответ 1

Я просто хэшировал 58 тысяч слов на английском языке (нашёл здесь), как с прописными буквами, так и с заглавной буквой. Знаете, сколько столкнулись? Два: "Братья и сестры" и "Тегеран" (альтернативное написание "Тегеран" ).

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

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

Если вам интересно, вот мой код (он использует Guava):

    List<String> words = CharStreams.readLines(new InputStreamReader(StringHashTester.class.getResourceAsStream("corncob_lowercase.txt")));
    Multimap<Integer, String> wordMap = ArrayListMultimap.create();
    for (String word : words) {
        wordMap.put(word.hashCode(), word);
        String capitalizedWord = word.substring(0, 1).toUpperCase() + word.substring(1);
        wordMap.put(capitalizedWord.hashCode(), capitalizedWord);
    }

    Map<Integer, Collection<String>> collisions = Maps.filterValues(wordMap.asMap(), new Predicate<Collection<String>>() {
        public boolean apply(Collection<String> strings) {
            return strings.size() > 1;
        }
    });

    System.out.println("Number of collisions: " + collisions.size());
    for (Collection<String> collision : collisions.values()) {
        System.out.println(collision);
    }

Изменить

Кстати, если вам интересно, тот же тест с вашей хэш-функцией имел 13 столкновений по сравнению с String.hashCode 1.

Ответ 2

Извините, но нам нужно добавить немного холодной воды для этой идеи.

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

  2. Никто в здравом уме не ожидал бы, что String.hashCode будет без коллизий 1. Это просто не предназначено для этого. (Если вы хотите использовать хеширование без коллизий, используйте алгоритм криптографического хеширования... и заплатите за него.) String.hashCode() разработан для того, чтобы быть достаточно хорошим во всем домене всех строк... и быстро.

  3. Предполагая, что вы могли бы изложить более веские доводы, это не место, чтобы заявить это. Вам нужно поднять эту проблему с людьми, которые имеют значение - с командой разработчиков Oracle Java.

  4. Текущий алгоритм для String::hashCode был частью спецификации javadoc для String начиная с Java 1.2. (И алгоритм почти наверняка восходит к Java 1.0 и более ранним версиям.) Если алгоритм был изменен, это было бы критическим изменением для некоторых приложений. Этого, вероятно, достаточно, чтобы убить идею.

  5. Команда разработчиков Java собирается сопоставить преимущества такого изменения с затратами на его реализацию, для них и для каждого пользователя Java.

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


1 - "Highly collision free hashing", is an idea / term that I pulled out of the air for the purposes of this answer. Sorry. However, the gist is that the probability of a hashcode collision for 2 strings should be independent of how related they are. So for instance "AA" and "bz" are related by virtue of having the same length. Obviously, this idea needs more thought. And it is also obvious that "relatedness" in the sense I'm talking about is not measurable ... sort of like Kolmogorov Complexity.)

Ответ 3

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

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

Ответ 4

Это довольно эффективно, но и просто. Все возможные строчные слова (ASCII) до шести букв или все числа длиной до шести цифр имеют уникальный hashCode(). то есть хэш-код, как базовое число 31. Использование большего числа имеет свои проблемы. Фактор 257 оставил бы каждый восьмой бит не особенно случайным, так как все символы ASCII имеют 0 верхний бит. Более высокий коэффициент приведет к дублированию хэш-кодов для пяти и шести цифр/букв.

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

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

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