Joda-time - наиболее известная и активно поддерживаемая/используемая библиотека проекта joda. Как гордо говорится на её странице, это "де-факто стандартная библиотека для работы с датами и временем в java". Реализация как Date, так и Calendar в java core имеют серьёзные недостатки, из-за чего joda-time и стала такой популярной. В качестве подтверждения популярности и качества библиотеки можно рассматривать предложения о включении её в состав jdk 7 (правда, эти ожидания так и не оправдались, зато появились новые - о включении в jdk 8).
Вот несколько причин, по которым joda-time появилась и стала востребованной: простота в использовании, лёгкая расширяемость, большой перечень возможностей, 100% совместимость со стандартными классами jdk, улучшенные характеристики производительности, поддержка нескольких (в настоящее время - 8) календарей, хорошее тестовое покрытие, полная документация, зрелость, открытый исходный код. Подробнее обо всём этом можно почитать прямо на главной странице проекта. Далее в этом посте - инфо, примеры использования joda-time и полезные ссылки.
Практически ни одно крупное приложение не обходится без работы с датами и временем; часто им требуется знать текущее время или какое-то время в прошлом/будущем, а иногда ещё и высчитывать разницу между ними. Кодировать эти операции с jdk можно, но сложно и нудно. joda-time позволяет делать это проще и понятнее. Проверим.
Пусть нам необходимо зафиксировать момент времени, скажем, ровно в полночь 01.08.2012. Как это сделать?
Строго говоря, если вам так уже необходимы изменяемые (mutable) объекты, joda-time предоставит такую возможность.
Дело в том, что в библиотеке имеются два интерфейса - ReadableХХХ и ReadWritableХХХ (где ХХХ - DateTime, Instant, Period и др.).
Единственное отличие интерфейсов - в способности к изменению - первые immutable, вторые mutable. Соответственно, вместо того же DateTime применяйте MutableDateTime - и будет счастье (вернее, изменяемые объекты).
Если подойти к анализу библиотеки совсем строго, то некоторые вопросы/недочёты можно-таки найти.
Вот несколько причин, по которым joda-time появилась и стала востребованной: простота в использовании, лёгкая расширяемость, большой перечень возможностей, 100% совместимость со стандартными классами jdk, улучшенные характеристики производительности, поддержка нескольких (в настоящее время - 8) календарей, хорошее тестовое покрытие, полная документация, зрелость, открытый исходный код. Подробнее обо всём этом можно почитать прямо на главной странице проекта. Далее в этом посте - инфо, примеры использования joda-time и полезные ссылки.
ПОКАЖИТЕ МНЕ РАЗНИЦУ!
Практически ни одно крупное приложение не обходится без работы с датами и временем; часто им требуется знать текущее время или какое-то время в прошлом/будущем, а иногда ещё и высчитывать разницу между ними. Кодировать эти операции с jdk можно, но сложно и нудно. joda-time позволяет делать это проще и понятнее. Проверим.
Пусть нам необходимо зафиксировать момент времени, скажем, ровно в полночь 01.08.2012. Как это сделать?
- java.util.Date? Доступны только два конструктора - без параметров (для фиксации текущего момента времени) и с одним параметром типа long - количества миллисекунд, прошедших "от начала времён" - оба нам явно не подходят.
- java.util.Calendar? Уже лучше, можно решить задачу так:
Calendar calendar = Calendar.getInstance();
calendar.set(2012, Calendar.AUGUST, 1, 0, 0, 0);
- org.joda.time? Решение выглядит так:
DateTime dateTime = new DateTime(2012, 8, 1, 0, 0, 0);Пока особенно большой разницы не заметно - подумаешь, одна строка вместо двух! Но давайте усложним задачу - попробуем добавить к зафиксированной дате 90 дней (например, период действия какой-то учётной записи) и распечатать результат.
Listing 1. Добавление 90 дней и форматирование вывода в Calendar
Calendar calendar = Calendar.getInstance();
calendar.set(2012, Calendar.AUGUST, 1, 0, 0, 0);
// add 90 days and format
calendar.add(Calendar.DAY_OF_MONTH, 90);
SimpleDateFormat sdf = new SimpleDateFormat("E yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(calendar.getTime()));
Listing 2. Добавление 90 дней и форматирование вывода в joda-timeЗдесь одна и та же операция записывается всего двумя строками вместо пяти. А если приложению потребуется получить дату последнего дня той недели, которая будет спустя 2 месяца и 5 дней с сегодняшней даты? Пытаться реализовать это на Calendar даже не хочется. Но на joda-time результат выглядит вполне понятным и элегантным:
DateTime dateTime = new DateTime(2012, 8, 1, 0, 0, 0);
// add 90 days and format
System.out.println(dateTime.plusDays(90).toString("E yyyy-MM-dd HH:mm:ss"));
Listing 3. joda-time и путь к спасениюРезультатом выполнения будет "Вс 2012-10-07 00:00:00" - можете проверить!
DateTime dateTime = new DateTime(2012, 8, 1, 0, 0, 0);
//tricky task
System.out.println(
dateTime.plusMonths(2).plusDays(5).dayOfWeek().withMaximumValue().toString("E yyyy-MM-dd HH:mm:ss"));
КЛЮЧЕВЫЕ КОНЦЕПЦИИ
- Неизменность (immutability)
Строго говоря, если вам так уже необходимы изменяемые (mutable) объекты, joda-time предоставит такую возможность.
Дело в том, что в библиотеке имеются два интерфейса - ReadableХХХ и ReadWritableХХХ (где ХХХ - DateTime, Instant, Period и др.).
Единственное отличие интерфейсов - в способности к изменению - первые immutable, вторые mutable. Соответственно, вместо того же DateTime применяйте MutableDateTime - и будет счастье (вернее, изменяемые объекты).
- Момент времени (instant)
- Частичное время (partial)
- Хронология (chronology)
Это концепция, лежащая в самом сердце joda-time (и описываемая одноимённым абстрактным классом). Фактически хронология - это календарь, система исчисления времени, и одновременно каркас для выполнения операций со временем. joda-time поддерживает 8 хронологий, в том числе ISO (по умолчанию), григорианский, коптский, арабский, юлианский, буддийский и др. календарные системы.
- Временная зона (time zone)
Временная зона - это географическое размещение относительно Гринвича, используемое для вычисления времени. Чтобы с точностью знать время, когда произошло то или иное событие, важно знать, где это событие случилось. Любые важные операции со временем должны выполняться с учётом временных зон (в виде исключения временные зоны можно не учитывать, если расчёты делаются для одной зоны и результаты не будут использоваться в других зонах). Данная концепция реализуется в joda-time в классе DateTimeZone. По умолчанию при расчётах библиотека использует зону, получаемую из настроек системы (на котором выполняется прикладной код); дополнительно многие конструкторы и методы имеют реализации для задания нужной зоны.
КАК СОЗДАТЬ ОБЪЕКТЫ joda-time?
Давайте поближе познакомимся с классами joda-time, которые обычно используются для работы с библиотекой. Как правило, их конструкторы имеют несколько реализаций с разными параметрами:
* текущее системное время;
* заданный момент времени (или частичный временной отрезок) с возможностью уточненить значения конкретных полей для достижения необходимой точности;
* заданный момент времени (или частичный временной отрезок) в миллисекундах;
* другой объект (например, java.util.Date или другой joda объект).
* текущее системное время;
* заданный момент времени (или частичный временной отрезок) с возможностью уточненить значения конкретных полей для достижения необходимой точности;
* заданный момент времени (или частичный временной отрезок) в миллисекундах;
* другой объект (например, java.util.Date или другой joda объект).
Попробуем поэкспериментировать с конструкторами.
- Момент времени. joda-time реализует концепцию момента времени через классы, имплементирующие интерфейс ReadableInstant.
- DateTime: класс, который используется наиболее часто. Он фиксирует момент времени с точностью до миллисекунд. Он всегда связан с временной зоной DateTimeZone, которая — если вы не уточнили отдельно — по умолчанию является системной таймзоной вашего компьютера. Как создать такой объект?
- На основе системного времени:
DateTime dateTime1 = new DateTime();
DateTime dateTime2 = DateTime.now();
- Явно задавая значения полей:
DateTime dateTime = new DateTime(
2012, //year
8, // month
1, // day
5, // hour (midnight is zero)
30, // minute
0, // second
10 // milliseconds
);
- На основе количества миллисекунд с начала эпохи:
DateTime dateTime = new DateTime(timeInMillis);
- На основе другого объекта:
Listing 4. Передача различных объектов в конструктор DateTime
DateTime dateTime;
// Use a Date class
java.util.Date jdkDate = obtainDateSomehow();
dateTime = new DateTime(jdkDate);
// Use a Calendar
java.util.Calendar calendar = obtainCalendarSomehow();
dateTime = new DateTime(calendar);
// Use another joda DateTime
DateTime anotherDateTime = obtainDateTimeSomehow();
dateTime = new DateTime(anotherDateTime);
// Use a String (must be formatted properly)
String timeString = "2006-01-26T13:30:00-06:00";
dateTime = new DateTime(timeString);
timeString = "2006-01-26";
dateTime = new DateTime(timeString);
- DateMidnight: то же, что и простой DateTime, однако та его часть, которая хранит время суток, всегда соответствует полночи. Этот и другие классы, рассматриваемые ниже, создаются подобным образом, что и DateTime. Поэтому в целях экономии места и времени их конструкторы будут описываться не так подробно.
- Далеко не всегда приложению требуется знание одновременно даты и времени (например, важны только год/месяц/день, или час дня, или день недели), поэтому часто оправдано применение концепции частичных временных отрезков, описываемой интерфейсом ReadablePartial.
- LocalDate: этот класс реализует комбинацию год/месяц/день (класс заменяет класс org.joda.time.YearMonthDay, применявшийся ранее и объявленный как deprecated с версии 1.3). Это удобно для хранения дат, например, дня рождения, даты заказа и пр.
LocalDate lDate1 = LocalDate.now();LocalDate lDate2 = new LocalDate(2012, 8, 1);
- LocalTime: этот класс реализует комбинацию типа час/минута/секунда.
LocalTime lTime1 = LocalTime.now();
LocalTime lTime2 = LocalTime.MIDNIGHT;
LocalTime lTime3 = new LocalTime(14, 43, 0, 0);
- Промежутки времени. Иногда полезно работать с объектами, выражающими промежутки времени. Для решения такой задачи joda-time предлагает три класса, т. ч. вы можете выбирать наиболее подходящую реализацию:
- продолжительность (Duration): абсолютный математический промежуток времени, выраженный в миллисекундах. Методы этого класса умеют конвертировать стандартные time-units (секунды, минуты, часы) на основе стандартных математических соотношений (таких, как 60 секунд в минуте и 24 часа в сутках). Класс полезен, если требуется выразить разницу во времени в миллисекундах;
- период (Period): реализует ту же концепцию, что и Duration, но делает это в более человекочитаемых терминах месяцев, недель, дней и т.д.;
- интервал (Interval): представляет специфический промежуток времени с определенным моментом, отмечая его границы. Интервал полуоткрыт, т. е. промежуток времени, хранящего в объекте типа Interval, включает начало промежутка, но не включает конец.
РАБОТА СО ВРЕМЕНЕМ В СТИЛЕ joda-time
Теперь, когда мы изучили создание полезных классов, давайте посмотрим, как использовать их для манипулирования и вычисления. Если всё, что вам требуется при работе со временем, это хранить информацию о дате и времени, тогда вам хватит и возможностей jdk. Но если вы замахиваетесь на вычисление времени - то joda-time вам в помощь. Вот несколько простых примеров.
Предположим, какая-то информация в приложении собирается на последний день предыдущего месяца. В этом случае нет нужды хранить время суток - достаточно будет формата год/месяц/день, т. ч. можно примерить LocalDate:
Listing 5. Расчёт даты
LocalDate lastDayOfPreviousMonth = LocalDate.now()
.minusMonths(1)
.dayOfMonth()
.withMaximumValue();
Возможно, следует подробнее остановиться на вызове dayOfMonth() в приведённом листинге (или на вызове dayOfWeek() в листинге 3). Это вызовы т. н. свойств (которые являются атрибутами некоторых объектов joda-time). Именно через свойства реализуется большая часть возможностей библиотеке по вычислению времени. Кроме рассмотренных dayOfMonth и dayOfWeek ещё есть yearOfCentury, monthOfYear, dayOfYear и др.
Теперь давайте пошагово изучим представленный в листинге 6 пример. Сначала мы получаем текущую дату, затем отнимаем один месяц, чтобы узнать "предыдущий месяц". Далее запрашиваем свойство dayOfMonth (день месяца), а у него получаем его максимальное значение - т. е. искомый последний день предыдущего месяца.
Обратите внимание, что вызовы организованы вместе в одну цепочку, что достигается за счёт неизменности применяемых joda-time объектов. Т. ч. достаточно сохранить результат последнего вызова, чтобы получить желаемое значение; и нет никакой необходимости в хранении промежуточных результатов.
А вот ещё несколько примеров:
Обратите внимание, что вызовы организованы вместе в одну цепочку, что достигается за счёт неизменности применяемых joda-time объектов. Т. ч. достаточно сохранить результат последнего вызова, чтобы получить желаемое значение; и нет никакой необходимости в хранении промежуточных результатов.
А вот ещё несколько примеров:
Listing 6. Несколько небольших примеров
DateTime now = DateTime.now();
DateTime twoWeeksBefore = now.minusWeeks(2);
DateTime tomorrow = now.plusDays(1);
DateTime twentyEightDaysAfterTomorrow = tomorrow.plusDays(28);
DateTime someSecondsLater = now.plusSeconds(156);
LocalDate then = LocalDate.now() // current time
.minusYears(5) // five years ago
.monthOfYear() // get 'monthOfYear' property
.setCopy(2) // set it to February
.dayOfMonth() // get 'dayOfMonth' property
.withMaximumValue(); // and find the last day of month
А вот как можно рассчитать разницу между датами:
Listing 7. Разница между датами
LocalDate now = LocalDate.now();
LocalDate then = new LocalDate(2005, 4, 20);
System.out.println(then.toString("E yyyy-MM-dd") + ", " + now.toString("E yyyy-MM-dd"));
// difference in days, or months, or other time units
System.out.println("Diff in days: " + Days.daysBetween(then, now).getValue(0));
System.out.println("Diff in months: " + Months.monthsBetween(then, now).getValue(0));
// period between two dates
Period period = new Period(then, now);
System.out.println(period.toString());
Примеров можно написать ещё много, но, я думаю, вы поняли идею. Вспомните, какие задачи по работе со временем приходилось решать вам в ваших приложений, и попробуйте решить их с помощью joda-time.
СОВМЕСТИМОСТЬ jdk И joda-time
Разработчики joda-time положили в основу библиотеки принцип, который можно назвать одним из важнейших - совместимость с jdk; в результате можно оставить уже имеющийся основанный на jdk код и написать/переделать наиболее трудоёмкие операции на joda-time. Это значительно облегчает переход на новую библиотеку.
Listing 8. joda and jdk interoperability
//from java.util.Calendar
Calendar calendar = Calendar.getInstance();
calendar.set(2012, Calendar.AUGUST, 1, 0, 0, 0);
//to org.joda.time.DateTime to make the most difficult calculations
DateTime dateTime = new DateTime(calendar.getTimeInMillis());
dateTime = dateTime.plusMonths(2).plusDays(5);
System.out.println(
dateTime.dayOfWeek().withMaximumValue().toString("E MM/dd/yyyy HH:mm:ss.SSS"));
// back to calendar in this way
calendar.setTime(dateTime.toDate());
// or in this one
calendar = dateTime.toCalendar(Locale.getDefault());
// from DateMidnight
DateMidnight dateMidnight = DateMidnight.now();
Date dateFromDM = dateMidnight.toDate();
// from LocalDate through one extra hoop
LocalDate localDate = LocalDate.now();
Date dateFromLD = localDate.toDateMidnight().toDate();
В приведённом примере мы фиксируем время в Calendar, далее переходим к DateTime для вычисления и форматирования, а затем снова возвращаемся к Calendar! При необходимости к стандартному Date можно привести и классы ReadablePartial (правда, через одно дополнительное преобразование). Действительно, очень удобная фича.
ФОРМАТИРОВАНИЕ
Форматирование времени в jdk вполне адекватное, но можно было бы и попроще - зачем создавать отдельный объект SimpleDateFormat?
В joda-time достаточно вызвать toString() на joda объекте, при необходимости передав ему стандартную ISO-8601 или jdk-совместимую строку с правилом форматирования - и получить желаемый результат (строго говоря, в библиотеке есть-таки класс DateTimeFormatter, если кто-то без него просто не может).
В joda-time достаточно вызвать toString() на joda объекте, при необходимости передав ему стандартную ISO-8601 или jdk-совместимую строку с правилом форматирования - и получить желаемый результат (строго говоря, в библиотеке есть-таки класс DateTimeFormatter, если кто-то без него просто не может).
Listing 9. ISO-8601
DateTime dateTime = DateTime.now();
dateTime.toString(ISODateTimeFormat.basicDateTime());
dateTime.toString(ISODateTimeFormat.basicDateTimeNoMillis());
dateTime.toString(ISODateTimeFormat.basicOrdinalDateTime());
dateTime.toString(ISODateTimeFormat.basicWeekDateTime());
20120802T114141.219+0300
20120802T114141+0300
2012215T114141.219+0300
2012W314T114141.219+0300
Listing 10. jdk-совместимый формат
DateTime dateTime = DateTime.now();
dateTime.toString("MM/dd/yyyy hh:mm:ss.SSSa");
dateTime.toString("dd-MM-yyyy HH:mm:ss");
dateTime.toString("EEEE dd MMMM, yyyy HH:mm:ssa");
dateTime.toString("MM/dd/yyyy HH:mm ZZZZ");
dateTime.toString("MM/dd/yyyy HH:mm Z");
08/02/2012 11:41:41.219AM
02-08-2012 11:41:41
четверг 02 Август, 2012 11:41:41AM
08/02/2012 11:41 Asia/Baghdad
08/02/2012 11:41 +0300
НЕДОСТАТКИ ЕСТЬ?
Если подойти к анализу библиотеки совсем строго, то некоторые вопросы/недочёты можно-таки найти.
- Ограничение точности до миллисекунд. С одной стороны, в некоторых приложениях высокая точность обработки времени может быть весьма важна, а joda-time по каким-то своим причинам ограничивает разработчика. С другой стороны, высокая точность приводит к большим числам, для хранения которых требуется уже не примитив long, а класс BigInteger, что приводит к серьёзному проигрышу в перформансе. А ограничение именно до миллисекунд обусловлено концепцией совместимости с jdk и той же производительностью; к тому же, для подавляющего большинства приложений этой точности вполне достаточно. В результате разработчики остановились на миллисекундах.
- joda-time разрешает использовать изменяемые (mutable) версии классов. В общем, кто-то рад, что такая возможность есть, а кто-то недоволен, что она есть.
- Достаточно большой размер библиотеки - в версии 2.1 более 150 классов, которые надо изучить для эффективного применения. Впрочем, начать работать просто - можно ограничиться тем же org.joda.time.DateTime, а затем по мере необходимости узнавать другие возможности. К тому же, эти классы предоставляют действительно богатый функционал, вовсе не сложный для понимания.
- В случае изменения какие-то правил, связанных со временем (например, изменение часовых поясов, правил перехода на летнее/зимнее время и т. п.) joda-time необходимо обновлять для актуализации.
- Если в joda-time к дате "31 мая" добавить 1 месяц (date.plusMonths(1)), то получим "30 июня", и никак иначе. А "30 июня" + 1 месяц = "30 июля"!
- Если не знаешь с уверенностью, что делаешь, отлаживаться дебаггером не очень удобно, во время выполнения об объекте можно получить немного информации. Лучше работать с javadoc и user guide'ом.
РАЗНОЕ И ССЫЛКИ
Q: Как вообще произносить название библиотеки?
A: The 'J' in 'Joda' is pronounced the same as the 'J' in 'Java'.
Joda-Time v2.0, пост автора проекта Stephen Colebourne о выходе версии 2.0 (если быть точным, то на момент публикации данного поста номер последней версии - 2.1). Около десяти лет развития говорят о зрелости продукта.
Переводной пост "Способы сравнения объектов дат в Java" с примерами, можно перейти и к оригиналу статьи. Становится понятнее, почему joda-time проще в использовании.
Потокобезопасный DateFormat, сравнение производительности операций parse/format в различных вариантах реализации. Наглядные характеристики производительности; цифры, конечно, у всех разные, но общая тенденция понятна. Ссылка на исходный код тестов также есть в посте, т. ч. можно скачать и поэкспериментировать самостоятельно.
Работа с Date и TimeZone, простая, но крайне понятная статья про основные проблемы.
Допустимые шаблоны даты и времени (y, M, d, h и пр.) в java.text.SimpleDateFormat. А также стандарт ISO-8601 описания даты и времени в международном контексте.
date4j, маленький проект, реализующий донельзя упрощённую модель работы с датами/временем и предназначенную в основном для взаимодействия с БД.
Краткий обзор работы с Date/Calendar для новичков.
Joda-Time. You can't escape time. Why not make it easy? Отличная англоязычная статья про библиотеку, с поясняющими примерами, которая послужила основой для данного поста.
A: The 'J' in 'Joda' is pronounced the same as the 'J' in 'Java'.
Joda-Time v2.0, пост автора проекта Stephen Colebourne о выходе версии 2.0 (если быть точным, то на момент публикации данного поста номер последней версии - 2.1). Около десяти лет развития говорят о зрелости продукта.
Переводной пост "Способы сравнения объектов дат в Java" с примерами, можно перейти и к оригиналу статьи. Становится понятнее, почему joda-time проще в использовании.
Потокобезопасный DateFormat, сравнение производительности операций parse/format в различных вариантах реализации. Наглядные характеристики производительности; цифры, конечно, у всех разные, но общая тенденция понятна. Ссылка на исходный код тестов также есть в посте, т. ч. можно скачать и поэкспериментировать самостоятельно.
Работа с Date и TimeZone, простая, но крайне понятная статья про основные проблемы.
Допустимые шаблоны даты и времени (y, M, d, h и пр.) в java.text.SimpleDateFormat. А также стандарт ISO-8601 описания даты и времени в международном контексте.
date4j, маленький проект, реализующий донельзя упрощённую модель работы с датами/временем и предназначенную в основном для взаимодействия с БД.
Краткий обзор работы с Date/Calendar для новичков.
Joda-Time. You can't escape time. Why not make it easy? Отличная англоязычная статья про библиотеку, с поясняющими примерами, которая послужила основой для данного поста.
ЗАКЛЮЧЕНИЕ
Когда дело доходит до обработки дат и времени, joda-time проявляет себя как весьма удобный и эффективный инструмент. Требуется ли рассчитать время, напечатать его или распарсить - всё выполняется удобно и понятно, на уровне интуиции. Положенные в основу библиотеки концепции позволили разработчикам создать инструмент, которым стОит пользоваться!
Спасибо, очень познавательная статья
ОтветитьУдалить