Введение в JSR 310: Дата и время в Java
Для работы с датой и временем в стандартной библиотеке Java существует два набора API: старый, включающий в себя такие классы, как Date и Calendar, и новый, опирающийся на стандарт ISO 8601 и включающий в себя классы из пакета java.time. В настоящее время разработчиками платформы Java рекомендуется использование новых API для работы с датой и временем.
В рамках данного материала я не буду разбирать использование старых API, остановлюсь лишь на их недостатках, которые и стали причинами появления популярной библиотеки JODA-Time, JSR 310 и новых API для работы с датой и временем.
Недостатки Date, Calendar и SimpleDateFormat
Первый недостаток старых API для работы с датой и временем заключается в мутабельности классов, из-за чего применение модификатора final для экземпляров классов Date или Calendar, например, не обеспечивает неизменяемость:
class SomeEntity {
// Модификатор final создаёт видимость неизменяемости
// Он запрещает только изменение ссылки на экземпляр класса Date,
// но не изменение его внутреннего состояния.
private final Date createdAt;
public Date getCreatedAt() {
return this.createdAt;
}
}
class SomeService {
public void someMethod(SomeEntity entity) {
// В результате этого любой код, имеющий доступ к createdAt или getCreatedAt,
// может поменять значение
entity.getCreatedAt().setHours(10);
}
}Частичным решением этой проблемы является возврат копии свойства в get-методе:
class SomeEntity {
private final Date createdAt;
public Date getCreatedAt() {
return (Date) this.createdAt.clone();
}
}Однако это не решает проблемы в целом.
Вторая проблема заключается в том что эти классы не являются потокобезопасными при своей изменяемости. Особенно странно это выглядит в случае SimpleDateFormat, из-за чего такой код в многопоточной среде может стать причиной некорректного поведения программы:
import java.text.SimpleDateFormat;
class SomeController {
SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
SomeService someService;
String getSomeEntityPage(int entityId, Model model) {
var entity = this.someService().getSomeEntity(entityId);
// Если несколько пользователей одновременно будут пользоваться программной,
// то получаемое свойство createdAt далеко не всегда будет соответствовать
// реальному значению
model.set("createdAt", this.dateFormat.format(entity.getCreatedAt()));
return "some_view";
}
}Причина такого поведения кроется в том, что DateFormat , от которого наследуется SimpleDateFormat, содержит свойство класса Calendar, в котором хранится обрабатываемая метка времени. И значение этого свойства изменяется при каждом вызове методов format и parse.
Разработчики JDK рекомендуют в таких случаях использовать синхронизацию:
import java.text.SimpleDateFormat;
class SomeController {
final SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm");
SomeService someService;
String getSomeEntityPage(int entityId, Model model) {
var entity = this.someService().getSomeEntity(entityId);
// Синхронизация решает проблему, но не делает работу с DateFormat понятнее
synchronized (this.dateFormat) {
model.set("createdAt", this.dateFormat.format(entity.getCreatedAt()));
}
return "some_view";
}
}Третья проблема заключается в миллисекундной точности. Для Date и Calendar минимальная единица времени — миллисекунда. Для большинства случаев такой точности достаточно, но вполне реальны случаи, когда требуется точность на уровне микросекунд или даже наносекунд, а современные операционные системы, JVM и процессоры способны обеспечивать точность на уровне сотен наносекунд.
Четвёртая проблема заключается в неочевидности и плохом дизайне API.
- Классы разбросаны по пакетам неочевидным образом:
Date— в пакетеjava.util, а есть ещё аналогичный класс вjava.sql, который ещё и расширяет исходныйDate,SimpleDateFormat— в пакетеjava.textи т.д. - При изменении года в
Calendarили создании экземпляраDateгод указывается относительно 1900 года (т.е. для 2026 года это 126) - Январю соответствует значение 0
- Первый день недели — воскресенье, значение которого — 1
- Неочевидное именование методов, например в
Calendar:void set(int field, int value)иvoid set(int year, int month, int date) - Составные части даты и времени описаны константами типами
int, т.к. на момент создания классовDate(Java 1.0) иCalendar(Java 1.1) ещё не существовали перечисления (Java 1.5).
JODA-Time
Описанные выше и прочие проблемы при работе с датой и временем в Java побудили сообщество к созданию одной из самых популярных библиотек в экосистеме Java (начиная с 1.3) — JODA-Time, которая поддерживает стандарт ISO 8601. Библиотека JODA-Time оказала настолько существенное влияние на работу с датой и временем в Java, что её наработки были включены в Java 8 в составе JSR 310, автором которых является Стивен Колборн — автор библиотеки JODA-Time.
Начиная с Java 8 именно классы, входящие в пакет java.time, являются предпочтительными для работы с датой и временем. А для проектов на Java более ранних версий рекомендуется использовать JODA-Time.
Стандарт ISO 8601
Стандарт ISO 8601 описывает формат метки времени и временных интервалов. Полная метка выглядит следующим образом: 2026-06-15T16:10:15.1234+05:00[Asia/Yekaterinburg], где:
2026-06-15— дата16:10:15.1234— время+05:00— смещение часового пояса относительно UTC, + используется для часовых поясов к востоку отUTC, - — для часовых поясов к западу.[Asia/Yekaterinburg]— идентификатор часового пояса
UTC — всемирное координированное время, в общих чертах оно является современной версией GMT — среднего времени по Гринвичу. Метка времени для UTC имеет собственный формат: 2026-06-15T11:10:15.1234Z, где вместо смещения и идентификатора часового пояса используется буква Z.
Обратите внимание на то, что часовой пояс определяется одновременно и смещением относительно UTC и идентификатором. Делается это по нескольким причинам:
- Людям удобнее пользоваться идентификаторами или названиями часовых поясов, чем смещениями (пример: 10 утра по Московскому времени или 7 вечера по Восточно-Европейскому времени).
- В некоторых странах до сих пор существует практика перехода на летнее время (DST, Daylight saving time), из-за чего один и тот же часовой пояс может иметь разные смещения в зависимости от времени года.
- Часовые пояса и их смещения могут изменяться, из-за чего вычисление смещений по названиям часовых поясов требует актуальности локальных баз данных часовых поясов.
JSR 310 предоставляет несколько классов в пакете java.time, работающих с разными форматами метки времени:
Instantработает с меткой времени в формате UTC:2026-06-15T11:10:15.1234ZZonedDateTime— с полной меткой времени:2026-06-15T16:10:15.1234+05:00[Asia/Yekaterinburg]OffsetDateTime— с меткой времени без названия часового пояса:2026-06-15T16:10:15.1234+05:00LocalDateTime— с меткой времени без часового пояса:2026-06-15T16:10:15.1234
Кроме этого предоставлены классы для работы с составными частями метки времени:
OffsetTime— с временем со смещением часового пояса:16:10:15.1234+05:00LocalDate— с датой без часового пояса:2026-06-15LocalTime— с временем без часового пояса:16:10:15.1234DayOfWeek— enum с днями недели:MONDAY,TUESDAYи т.д.Month— enum с месяцами:JANUARY,FEBRUARYи т.д.MonthDay— с месяцем и датой:--06-15Year— с годом:2026YearMonth— с годом и месяцем:2026-06ZoneId— с названием часового пояса:Asia/YekaterinburgZoneOffset— со смещением часового пояса:+05:00
Интервалы времени
Также ISO 8601 определяет формат временных интервалов или промежутков, который может иметь вид PnYnMnDTnHnMnS или PnW. Формат промежутка времени содержит следующие символы:
P— символ, указывающий на то, что эта строка является интервалом времени, должен быть в начале строкиY— количество годовM— количество месяцевW— количество недель, может использоваться только отдельно от других единиц времениD— количество днейT— разделитель, отделяющий временную часть промежутка времениH— количество часовM— количество минут, не следует путать сMдоT, который описывает количество месяцевS— количество секунд
Для всех единиц времени кроме секунд n может быть только целым числом, секунды могут описываться числом с плавающей точкой. Ниже приведены примеры интервалов времени:
P1D— 1 деньPT7H— 7 часовP1DT7H— 1 день 7 часовPT0.05S— 0,05 секундыP2W— 2 недели
Для работы с промежутками времени в Java существуют классы Duration и Period.
В следующих статьях я более подробно разберу использование API для работы с датой и временем.
