Почему Java Date.after () возвращает true, если дата на самом деле более ранняя?

У меня была программа, в которой я сравнивал два свидания друг с другом; хотя date1 был до date2, date1.after(date2) вернул true. Часовые пояса не влияли; обе даты были в UTC.

import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

public class Test {
    public static void main(String[] args) throws Exception {
        TimeZone.setDefault(TimeZone.getTimeZone("Etc/UTC"));
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        Date date1 = dateFormat.parse("2018-07-27 01:22:14.077");
        Date date2 = new Timestamp(1532654534390l);
        System.out.println(dateFormat.format(date1));
        System.out.println(dateFormat.format(date2));
        System.out.println(date1.getTime() > date2.getTime());
        System.out.println(date1.after(date2));
    }
}

Это выводит:

2018-07-27 01:22:14.077
2018-07-27 01:22:14.390
false
true

Что здесь происходит?


In my real program, date1 is parsed from a log file and date2 is retrieved from the database by Hibernate, which causes the different data types. Even though I found the root cause and know how to avoid the problem, I'm still very interested in solutions which prevent this pitfall.

Прочитать документация. Он предостерегает вас от обращения с Timestamp как с Date, предсказывает поведение, которое вы видели, и объясняет, почему. Здесь нет новостей.
Basil Bourque 09.09.2018 18:53

@BasilBourque верно, но в реальной программе я определяю date2 как java.util.Date, а Hibernate возвращает мне java.sql.Timestamp. Я не заметил, что 6-8 лет до сих пор работаю над этим приложением.

Glorfindel 09.09.2018 19:25

Вот почему этот хакерский метод проектирования классов настолько коварен, что компилятор не знает и не может обеспечить соблюдение своей политики pretend-Timestamp-is-not-Date. К счастью, Hibernate поддерживает java.time, чтобы помочь положить конец долгому кошмару Date / Calendar / Timestamp.

Basil Bourque 09.09.2018 19:43

Когда я буду переделывать приложение, я обязательно учту это. Тем не менее, другие люди могут застрять в Java 7 по тем или иным причинам.

Glorfindel 09.09.2018 19:45

Если вам нужны такие операции в Java 7, рассмотрите библиотека ThreeTen Backport, бэкпорт java.time для Java 6 и 7.

Ole V.V. 10.09.2018 11:35

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

Ole V.V. 10.09.2018 11:36

@ OleV.V. Я не использую эти литералы в реальном приложении, это просто MCVE :) Я вижу, что Василий и вы используете символы подчеркивания, это хороший трюк.

Glorfindel 10.09.2018 11:39
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
1
7
1 062
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Основная «проблема» здесь в том, что java.sql.Timestamp, расширяя java.util.Date, сохраняет миллисекунды не в назначенном поле (fastTime, эквивалент Время Unix), а в отдельном поле nanos. Метод after учитывает только поле fastTime (что имеет смысл, поскольку его можно использовать для всех объектов Date).

В этой ситуации происходит то, что fastTimeTimestamp - это округлено в меньшую сторону от 1532654534390 до 1532654534000, что ниже 1532654534077 другой даты (и ниже означает более раннюю дату). Следовательно, after() и before() в этом случае ненадежны; решение состоит в том, чтобы использовать getTime() (который перегружен для Timestamp, чтобы обеспечить правильное значение) для обеих дат и сравнить их.

В идеале вы все равно должны использовать java.time API

OneCricketeer 09.09.2018 18:00

tl; dr

Используйте современные классы java.time, а не ужасные устаревшие классы даты и времени.

Instant
.ofEpochMilli( 1_532_654_534_390L ) 
.isAfter(
    LocalDateTime
    .parse( 
        "2018-07-27 01:22:14.077"
        .replace( " " , "T" ) 
    )
    .atOffset( 
        ZoneOffset.UTC
    )
    .toInstant()
)

Док говорит: не используйте объект Timestamp как Date

Ваш код:

Date date2 = new Timestamp…  // Violates class documentation. 

… Нарушает договор, установленный в документации класса.

Due to the differences between the Timestamp class and the java.util.Date class mentioned above, it is recommended that code not view Timestamp values generically as an instance of java.util.Date. The inheritance relationship between Timestamp and java.util.Date really denotes implementation inheritance, and not type inheritance.

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

Конечно, эта политика «притвориться не подклассом» - смехотворно плохая конструкция класса. Этот взлом является одной из причин много для никогда не используйте эти классы.

Задокументировано поведение, которое вы видели в отношении несоответствия долей миллисекунд и наносекунд:

Note: This type is a composite of a java.util.Date and a separate nanoseconds value. Only integral seconds are stored in the java.util.Date component. The fractional seconds - the nanos - are separate. The Timestamp.equals(Object) method never returns true when passed an object that isn't an instance of java.sql.Timestamp, because the nanos component of a date is unknown. As a result, the Timestamp.equals(Object) method is not symmetric with respect to the java.util.Date.equals(Object) method. Also, the hashCode method uses the underlying java.util.Date implementation and therefore does not include nanos in its computation.

java.time

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

Эти унаследованные классы были вытеснены несколько лет назад классами java.time.

Разберите вашу строку ввода.

LocalDateTime ldt = LocalDateTime.parse( "2018-07-27 01:22:14.077".replace( " " , "T" ) ;  // Without a time zone or offset, this value has no specific meaning, is *not* a point on the timeline. 

По-видимому, вы знаете, что входная строка неявно представляла момент в формате UTC.

OffsetDateTime odt = ldt.atOffset( ZoneOffset.UTC ) ;  // Assign an offset-from-UTC to give the date and time a meaning as an actual point on the timeline. 

Проанализируйте другой ввод, по-видимому, количество миллисекунд с первого момента 1970 года по всемирному координированному времени. Класс Instant более простой, чем OffsetDateTime, момент в формате UTC, всегда по определению в формате UTC.

Instant instant = Instant.ofEpochMilli( 1_532_654_534_390L ) ;  // Translate a count of milliseconds from 1970-01-01T00:00:00Z into a moment on the timeline in UTC. 

Сравнивать.

Boolean stringIsAfterLong = odt.toInstant().isAfter( instant ) ;

О java.time

Фреймворк java.time встроен в Java 8 и новее. Эти классы заменяют неудобные старые классы даты и времени наследие, такие как java.util.Date, Calendar, & SimpleDateFormat.

Проект Джода-Тайм, теперь в Режим технического обслуживания, советует перейти на классы java.time.

Чтобы узнать больше, см. Учебник Oracle. И поищите в Stack Overflow множество примеров и объяснений. Спецификация - JSR 310.

Вы можете обмениваться объектами java.time напрямую с вашей базой данных. Используйте Драйвер JDBC, совместимый с JDBC 4.2 или новее. Нет необходимости в строках, нет необходимости в классах java.sql.*.

Где взять классы java.time?

  • Java SE 8, Java SE 9, Java SE 10, Java SE 11 и более поздние версии - часть стандартного Java API с объединенной реализацией.
    • В Java 9 добавлены некоторые незначительные функции и исправления.
  • Java SE 6 и Java SE 7
    • Большая часть функциональности java.time перенесена на Java 6 и 7 в ThreeTen-Backport.
  • Android
    • Более поздние версии Android связывают реализации классов java.time.
    • Для более ранних версий Android (<26) проект ThreeTenABP адаптирует ThreeTen-Backport (упомянутый выше). См. Как использовать ThreeTenABP….

Проект ThreeTen-Extra расширяет java.time дополнительными классами. Этот проект является испытательной площадкой для возможных будущих дополнений к java.time. Здесь вы можете найти несколько полезных классов, таких как Interval, YearWeek, YearQuarter и более.

Другие вопросы по теме