У меня была программа, в которой я сравнивал два свидания друг с другом; хотя 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.
@BasilBourque верно, но в реальной программе я определяю date2 как java.util.Date, а Hibernate возвращает мне java.sql.Timestamp. Я не заметил, что 6-8 лет до сих пор работаю над этим приложением.
Вот почему этот хакерский метод проектирования классов настолько коварен, что компилятор не знает и не может обеспечить соблюдение своей политики pretend-Timestamp-is-not-Date. К счастью, Hibernate поддерживает java.time, чтобы помочь положить конец долгому кошмару Date / Calendar / Timestamp.
Когда я буду переделывать приложение, я обязательно учту это. Тем не менее, другие люди могут застрять в Java 7 по тем или иным причинам.
Если вам нужны такие операции в Java 7, рассмотрите библиотека ThreeTen Backport, бэкпорт java.time для Java 6 и 7.
Кроме того, избегайте строчных букв l для длинных литералов, их трудно отличить от цифры 1. Я рекомендую 1_532_654_534_390L для удобства чтения (к сожалению, между последней цифрой и L не допускается подчеркивание).
@ OleV.V. Я не использую эти литералы в реальном приложении, это просто MCVE :) Я вижу, что Василий и вы используете символы подчеркивания, это хороший трюк.




Основная «проблема» здесь в том, что java.sql.Timestamp, расширяя java.util.Date, сохраняет миллисекунды не в назначенном поле (fastTime, эквивалент Время Unix), а в отдельном поле nanos. Метод after учитывает только поле fastTime (что имеет смысл, поскольку его можно использовать для всех объектов Date).
В этой ситуации происходит то, что fastTimeTimestamp - это округлено в меньшую сторону от 1532654534390 до 1532654534000, что ниже 1532654534077 другой даты (и ниже означает более раннюю дату). Следовательно, after() и before() в этом случае ненадежны; решение состоит в том, чтобы использовать getTime() (который перегружен для Timestamp, чтобы обеспечить правильное значение) для обеих дат и сравнить их.
В идеале вы все равно должны использовать java.time API
Используйте современные классы 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.
Разберите вашу строку ввода.
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 8 и новее. Эти классы заменяют неудобные старые классы даты и времени наследие, такие как java.util.Date, Calendar, & SimpleDateFormat.
Проект Джода-Тайм, теперь в Режим технического обслуживания, советует перейти на классы java.time.
Чтобы узнать больше, см. Учебник Oracle. И поищите в Stack Overflow множество примеров и объяснений. Спецификация - JSR 310.
Вы можете обмениваться объектами java.time напрямую с вашей базой данных. Используйте Драйвер JDBC, совместимый с JDBC 4.2 или новее. Нет необходимости в строках, нет необходимости в классах java.sql.*.
Где взять классы java.time?
Проект ThreeTen-Extra расширяет java.time дополнительными классами. Этот проект является испытательной площадкой для возможных будущих дополнений к java.time. Здесь вы можете найти несколько полезных классов, таких как Interval, YearWeek, YearQuarter и более.
Timestampкак сDate, предсказывает поведение, которое вы видели, и объясняет, почему. Здесь нет новостей.