Статьи

Болезненное напоминание о нюансах даты Java

Мне больше не нужно больше использовать java.util.Date в эти дни, но недавно я решил сделать это, и мне напомнили о боли при использовании API, связанных с Java Date . В этой статье я рассмотрю несколько удивительных ожиданий API устаревшего параметризованного конструктора Date, который принимает шесть целых чисел .

В 2016 году разработчики Java, скорее всего, будут использовать новый API даты / времени Java 8, если пишут новый код в Java SE 8, или могут использовать стороннюю библиотеку даты / времени Java, такую ​​как Joda-Time, при использовании версии Java до Java 8. Недавно я решил использовать Date в очень простом инструменте на основе Java, который я хотел доставить в виде одного файла исходного кода Java (легко компилировать без инструмента сборки) и не зависеть от каких-либо внешних библиотек. Java SE. Средой развертывания для этого простого инструмента является Java SE 7, поэтому API даты / времени Java 8 не был выбран.

Одним из недостатков конструктора Date который принимает шесть целых чисел, является различие между этими шестью целыми числами и обеспечение их соответствия в правильном порядке. Даже при соблюдении правильного порядка, есть тонкие сюрпризы, связанные с указанием месяца и года. Возможно, самый простой способ правильно создать экземпляр объекта Date — через SimpleDateFormat . parse (String) или через конструктор Date (long) без устаревших значений, принимающий миллисекунды с нулевой эпохи.

Мой первый список кода демонстрирует создание Date представляющей «26 сентября 2016 года» с 0 часами, 0 минутами и 0 секундами. Этот листинг кода использует String для создания экземпляра Date помощью SimpleDateFormat.parse(String) .

1
2
3
final SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_FORMAT);
final Date controlDate = formatter.parse(CONTROL_DATE_TIME_STR);
printDate("Control Date/Time", controlDate);

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

1
2
3
=============================================================
= Control Date/Time -> Mon Sep 26 00:00:00 MDT 2016
=============================================================

Может быть соблазнительно использовать конструкторы Date которые принимают целые числа, для представления различных «полей» экземпляра Date , но они представляют ранее упомянутые нюансы.

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

1
2
3
4
5
// This will NOT be the intended Date of 26 September 2016
// with 0 hours, 0 minutes, and 0 seconds because both the
// "month" and "year" parameters are NOT appropriate.
final Date naiveDate = new Date(2016, 9, 26, 0, 0, 0);
printDate("new Date(2016, 9, 26, 0, 0, 0)", naiveDate);

Результат выполнения вышеуказанного кода не имеет ни того же месяца (октябрь, а сентябрь), ни того же года (не 2016), как в «контрольном» случае, показанном ранее.

1
2
3
=============================================================
= new Date(2016, 9, 26, 0, 0, 0) -> Thu Oct 26 00:00:00 MDT 3916
=============================================================

Месяц был на один позже, чем мы ожидали (октябрь, а не сентябрь), поскольку параметр месяца — это параметр, начинающийся с нуля, при этом январь представлен нулем, а сентябрь — 8 вместо 9. Один из самых простых способов справиться с Начинающийся с нуля месяц и более удобочитаемым вызовом конструктора Date является использование соответствующего поля java.util.Calendar для месяца. Следующий пример демонстрирует это с помощью Calendar.SEPTEMBER .

1
2
3
4
5
// This will NOT be the intended Date of 26 September 2016
// with 0 hours, 0 minutes, and 0 seconds because the
// "year" parameter is not correct.
final Date naiveDate = new Date(2016, Calendar.SEPTEMBER, 26, 0, 0, 0);
printDate("new Date(2016, Calendar.SEPTEMBER, 26, 0, 0, 0)", naiveDate);

Только что приведенный фрагмент кода исправляет спецификацию месяца, но год по-прежнему выключен, как показано в соответствующем выводе, который показан далее.

1
2
3
=============================================================
= new Date(2016, Calendar.SEPTEMBER, 26, 0, 0, 0) -> Tue Sep 26 00:00:00 MDT 3916
=============================================================

В этом году еще 1900 лет (3916 вместо 2016 года). Это связано с тем, что первым целочисленным параметром в конструкторе Date с шестью целыми Date будет год, указанный как год без 1900. Таким образом, в качестве первого аргумента укажите «2016», указав год как 2016 + 1900 = 3916. Чтобы исправить это, нам нужно вместо этого предоставить 116 (2016-1900) в качестве первого параметра int конструктору. Чтобы сделать это более читабельным для обычного человека, который нашел бы это удивительным, я хотел бы закодировать его буквально как 2016-1900, как показано в следующем листинге кода.

1
2
final Date date = new Date(2016-1900, Calendar.SEPTEMBER, 26, 0, 0, 0);
printDate("new Date(2016-1900, Calendar.SEPTEMBER, 26, 0, 0, 0)", date);

С использованным месяцем, начинающимся с нуля, и с предполагаемым годом, выраженным как текущий год, за вычетом 1900, Date корректно создается, как показано в следующем выходном списке.

1
2
3
=============================================================
= new Date(2016-1900, Calendar.SEPTEMBER, 26, 0, 0, 0) -> Mon Sep 26 00:00:00 MDT 2016
=============================================================

Документация Javadoc для Date действительно описывает эти нюансы, но это напоминание о том, что зачастую лучше иметь понятные и понятные API, которые не нуждаются в нюансах, описанных в комментариях. Javadoc для конструктора Date (int, int, int, int, int, int) действительно объявляет, что из него нужно вычесть 1900 год и что месяцы представлены целыми числами от 0 до 11. Он также описывает, почему это шесть целых чисел конструктор не рекомендуется : «Начиная с версии 1.1 JDK, заменен на Calendar.set (год + 1900, месяц, дата, часы, минуты, секунды) или GregorianCalendar (год + 1900, месяц, дата, часы, минуты, секунды).»

Подобный шестизначный конструктор GregorianCalendar (int, int, int, int, int, int) не считается устаревшим и, хотя он все еще ожидает параметр месяца, начинающийся с нуля, он не ожидает, что один из них вычтет фактический год к 1900 году при проверке параметр года. Когда месяц указывается с использованием соответствующей константы Calendar месяца, это делает вызов API гораздо более читабельным, если для года можно передать 2016 год, а для месяца — Calendar.SEPTEMBER .

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

  1. Если используется Java 8+, используйте Java 8 Date / Time API.
  2. Если вы используете версию Java до Java 8, используйте Joda-Time или другую улучшенную библиотеку Java.
  3. Если вы не можете использовать Java 8 или стороннюю библиотеку, используйте Calendar вместо Date в максимально возможной степени, особенно для создания экземпляров.
  4. Если в любом случае используется Date , создайте экземпляр Date с помощью подхода SimpleDateFormat.parse(String) или с помощью Date(long) для создания экземпляра Date на основе миллисекунд с нулевой эпохи.
  5. При использовании конструкторов Date принимающих несколько целых чисел, представляющих компоненты даты / времени по отдельности, используйте соответствующее поле Calendar месяца, чтобы сделать вызовы API более читабельными, и подумайте о написании простого компоновщика, чтобы «обернуть» вызовы конструктору с шестью целыми числами.

Мы можем многое узнать о том, что делает API полезным и легким для изучения, а что делает API более трудным для изучения с помощью API других людей. Надеемся, что полученные уроки помогут нам в написании наших собственных API. Конструктор Date(int, int, int, int, int, int) который был в центре внимания этого поста, представляет несколько проблем, которые делают API менее оптимальным. Несколько параметров одного и того же типа облегчают предоставление параметров не по порядку, а «ненатуральные» правила, связанные с указанием года и месяца, создают дополнительную нагрузку на разработчика клиента, чтобы он читал Javadoc, чтобы понять эти не столь очевидные правила.

Ссылка: Болезненное напоминание о дате Java Нюансы от нашего партнера JCG Дастина Маркса в блоге Inspired by Actual Events .