Java.util.Date equals() не работает должным образом

Проблема

У меня есть Map<Date, Foo> и список объектов из базы данных с свойством effectiveDate, и я хочу проверить, соответствуют ли ключи Date на моей карте любой из effectiveDate в базе данных - если да, делайте что-нибудь с Foo.

Код выглядит примерно так:

for (Bar bar : databaseBars) {
  Foo foo = new Foo();
  if (dateMap.containsKey(bar.getEffectiveDate()) {
    foo = dateMap.get(bar.getEffectiveDate());
  }
  // do stuff with foo and bar
}

Однако вызов dateMap.containsKey всегда возвращает false, хотя я уверен, что он иногда там.

Исследование

Как проверка работоспособности, я распечатал длинные значения дат, а также результаты вызова equals() и вызова compareTo():

for (Date keyDate : dateMap.keySet()) {
  if (keyDate == null) {
    continue; // make things simpler for now
  }

  Date effDate = bar.getEffectiveDate();

  String template = "keyDate: %d; effDate: %d; equals: %b; compareTo: %d\n";

  System.out.printf(template, keyDate.getTime(), effDate.getTime(), effDate.equals(keyDate), effDate.compareTo(keyDate));
}

Результаты:

keyDate: 1388534400000; effDate: 1388534400000; equals: false; compareTo: 0
keyDate: 1420070400000; effDate: 1388534400000; equals: false; compareTo: -1
keyDate: 1388534400000; effDate: 1420070400000; equals: false; compareTo: 1
keyDate: 1420070400000; effDate: 1420070400000; equals: false; compareTo: 0
keyDate: 1388534400000; effDate: 1388534400000; equals: false; compareTo: 0
keyDate: 1420070400000; effDate: 1388534400000; equals: false; compareTo: -1
keyDate: 1388534400000; effDate: 1420070400000; equals: false; compareTo: 1
keyDate: 1420070400000; effDate: 1420070400000; equals: false; compareTo: 0
keyDate: 1388534400000; effDate: 1388534400000; equals: false; compareTo: 0
keyDate: 1420070400000; effDate: 1388534400000; equals: false; compareTo: -1
keyDate: 1388534400000; effDate: 1420070400000; equals: false; compareTo: 1
keyDate: 1420070400000; effDate: 1420070400000; equals: false; compareTo: 0

Вопрос

1) Должны ли equals и compareTo согласиться? (Я предполагаю, что реализация java.util.Date по крайней мере должна попытаться выполнить рекомендацию java.lang.Comparable).

2) Док Date#equals говорит об этом:

Таким образом, два объекта Date являются равными тогда и только тогда, когда метод getTime возвращает одинаковое длинное значение для обоих.

... Похоже, метод getTime возвращает одинаковое длинное значение для обеих этих дат, но equal возвращает false. Любые идеи, почему это может произойти? Я искал высоко и низко, но я не нашел никого, описывающего ту же проблему.

P.S. Я застрял с помощью java.util.Date. Пожалуйста, не просто рекомендуйте JodaTime.

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

Ответ 1

Как Mureinik намекнул, и Sotirios Delimanolis указал более конкретно, проблема здесь в том, что реализация java.util.Date.

java.util.Date расширяется 3 классами в пакете java.sql, все из которых, похоже, делают подобные вещи и чье отличие в java не совсем ясное (похоже, причиной их существования является просто создание классов Java которые более точно сопоставляются с типами данных SQL) - для получения дополнительной информации об их различиях ознакомьтесь с этим очень подробным ответом.

Теперь, в том, что кажется серьезным недостатком дизайна, кто-то решил сделать equals() асимметричным с java.sql.Timestamp - timestamp.equals(date) может возвращать false, даже если date.equals(timestamp) возвращает значение true. Отличная идея.

Я написал несколько строк, чтобы увидеть, какие классы java.sql демонстрируют это смехотворное свойство - видимо, это просто Timestamp. Этот код:

java.util.Date utilDate = new java.util.Date();

java.sql.Date sqlDate = new java.sql.Date(utilDate.getTime());

System.out.println("sqlDate equals utilDate:\t" + sqlDate.equals(utilDate));
System.out.println("utilDate equals sqlDate:\t" + utilDate.equals(sqlDate));

java.sql.Time time = new java.sql.Time(utilDate.getTime());

System.out.println("time equals utilDate:\t\t" + time.equals(utilDate));
System.out.println("utilDate equals time:\t\t" + utilDate.equals(time));

java.sql.Timestamp timestamp = new java.sql.Timestamp(utilDate.getTime());

System.out.println("timestamp equals utilDate:\t" + timestamp.equals(utilDate));
System.out.println("utilDate equals timestamp:\t" + utilDate.equals(timestamp));

Уступает:

sqlDate equals utilDate:    true
utilDate equals sqlDate:    true
time equals utilDate:       true
utilDate equals time:       true
timestamp equals utilDate:  false
utilDate equals timestamp:  true

Поскольку java.util.HashMap использует parameter.equals(key) в нем реализацию containsKey() (а не key.equals(parameter)), этот один странный результат появляется в данной ситуации.

Итак, как обойти это?

1) Используйте клавишу Long на карте, а не Date (как отметил Мурейник) - поскольку java.util.Date и java.util.Timestamp возвращают одинаковое значение из getTime(), не имеет значения, какая реализация вы используете, ключ будет таким же. Этот способ кажется самым простым.

2) Стандартизируйте объект даты перед его использованием на карте. Этот способ требует немного большего количества работы, но мне кажется более желательным, поскольку он более четко показывает, что такое карта - куча Foo, каждый из которых хранится с моментом времени. Так я использовал, используя следующий метод:

public Date getStandardizedDate(Date date) {
  return new Date(date.getTime());
}

Для этого требуется дополнительный вызов метода (и вроде бы это смешно), но для меня заслуживает повышенная читаемость кода с участием Map<Date, Foo>.

Ответ 2

Объект A Date, возвращенный из базы данных, вероятно, будет java.sql.Timestamp, который не может быть равен объекту java.util.Date. Я бы просто забрал long из getTime() и использовал это как ключ в вашем HashMap.

Ответ 3

Часть 1: "Не следует equals соглашаться с compareTo? *

Нет; compareTo должен соглашаться с равными, но обратное не имеет значения.

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

Часть 2: Равные даты.

Вызовы базы данных возвратят java.sql.Date, хотя он присваивается java.util.Dare, поскольку он расширяет его, не будет равным, потому что класс отличается.

Работа может быть:

java.util.Date test;
java.sql.Date date;
if (date.equals(new java.sql.Date(test.getTime()))