Двойной в HashMap

Я думал использовать Double как ключ к HashMap, но я знаю, что сравнение с плавающей запятой небезопасно, что заставило меня задуматься. Является ли метод equals в классе Double небезопасным? Если это так, это означает, что метод hashCode, вероятно, также неверен. Это означало бы, что использование Double в качестве ключа к HashMap приведет к непредсказуемому поведению.

Может ли кто-нибудь подтвердить мои предположения здесь?

Ответы

Ответ 1

Короткий ответ: Не делайте этого

Длинный ответ: Вот как будет вычисляться ключ:

Фактическим ключом будет объект java.lang.Double, так как ключи должны быть объектами. Вот его метод hashCode():

public int hashCode() {
  long bits = doubleToLongBits(value);
  return (int)(bits ^ (bits >>> 32));
}

Метод doubleToLongBits() в основном принимает 8 байтов и представляет их как долго. Таким образом, это означает, что небольшие изменения в вычислении double могут означать много, и у вас будут ключевые промахи.

Если вы можете установить заданное количество точек после точки - умножить на 10 ^ (количество цифр после точки) и преобразовать в int (например, для двух цифр умножить на 100).

Это будет намного безопаснее.

Ответ 2

Я думаю, что ты прав. Хотя хэш удваивает ints, двойной может испортить хэш. Вот почему, как говорит Джош Блох в "Эффективной Java", когда вы используете double в качестве входа в хэш-функцию, вы должны использовать doubleToLongBits(), Аналогично, используйте floatToIntBits для поплавков.

В частности, чтобы использовать двойной как ваш хэш, следуя рецепту Джоша Блоха, вы бы сделали:

public int hashCode() {
  int result = 17;
  long temp = Double.doubleToLongBits(the_double_field);
  result = 37 * result + ((int) (temp ^ (temp >>> 32)));
  return result;
}

Это из пункта 8 Эффективной Java: "Всегда переопределяйте hashCode при переопределении равных". Его можно найти в этом pdf в главе из книги.

Надеюсь, что это поможет.

Ответ 3

Это зависит от того, как вы его используете.

Если вы довольны тем, что можете найти значение, основанное на одном и том же битовом шаблоне (или, возможно, эквивалентном, например +/- 0 и разных NaN), тогда это может быть хорошо.

В частности, все NaN в конечном итоге считаются равными, но +0 и -0 считаются разными. Из документов для Double.equals:

Обратите внимание, что в большинстве случаев для двух экземпляры класса Double, d1 и d2, значение d1.equals(d2) истинно, если и только если

d1.doubleValue() == d2.doubleValue() также имеет значение правда. Однако есть два Исключения:

  • Если d1 и d2 оба представляют Double.NaN, то метод равен возвращает true, хотя Double.NaN == Double.NaN имеет значение ложь.
  • Если d1 представляет +0.0, а d2 представляет -0.0, или наоборот, равный тест имеет значение false, даже хотя +0.0 == - 0.0 имеет значение true.

Это определение позволяет хэш-таблицам правильно работать.

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

Ответ 4

Проблема заключается не в хеш-коде, а в точности в удвоениях. Это вызовет некоторые странные результаты. Пример:

    double x = 371.4;
    double y = 61.9;
    double key = x + y;    // expected 433.3

    Map<Double, String> map = new HashMap<Double, String>();
    map.put(key, "Sum of " + x + " and " + y);

    System.out.println(map.get(433.3));  // prints null

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

Если вы используете

map.get(key)

он должен найти запись... []]

Ответ 5

Короткий ответ: Вероятно, это не сработает.

Честный ответ: все зависит.

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

Итак, вопрос в том, что ваши двойники генерируются таким образом, что вы знаете, что equals на самом деле означает equals? Если вы читаете или вычисляете значение, храните его в хеш-таблице, а затем позже читаете или вычисляете значение, используя точно такое же вычисление, тогда Double.equals будут работать. Но в противном случае это ненадежное: 1.2 + 2.3 не обязательно равно 3.5, оно может равняться 3.4999995 или что-то еще. (Не настоящий пример, я только что сделал это, но это то, что происходит.) Вы можете сравнивать поплавки и удваивать разумно надежно для меньшего или большего, но не для равных.

Ответ 6

Может быть BigDecimal получить, куда вы хотите пойти?

Ответ 7

Используется хэш двойника, а не сам двойной.

Изменить: Спасибо, Джон, я на самом деле этого не знал.

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

Ответ 8

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

private static final double key1 = 1.1+1.3-1.6;
private static final double key2 = 123321;
...
map.get(key1);

все будет хорошо, однако

map.put(1.1+2.3, value);
...
map.get(5.0 - 1.6);

будет опасным