Введение в JSR 310: Дата и время в Java

Введение в JSR 310: Дата и время в Java, image #1

Для работы с датой и временем в стандартной библиотеке 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.1234Z
  • ZonedDateTime — с полной меткой времени: 2026-06-15T16:10:15.1234+05:00[Asia/Yekaterinburg]
  • OffsetDateTime — с меткой времени без названия часового пояса: 2026-06-15T16:10:15.1234+05:00
  • LocalDateTime — с меткой времени без часового пояса: 2026-06-15T16:10:15.1234

Кроме этого предоставлены классы для работы с составными частями метки времени:

  • OffsetTime — с временем со смещением часового пояса: 16:10:15.1234+05:00
  • LocalDate — с датой без часового пояса: 2026-06-15
  • LocalTime — с временем без часового пояса: 16:10:15.1234
  • DayOfWeek — enum с днями недели: MONDAY, TUESDAY и т.д.
  • Month — enum с месяцами: JANUARY, FEBRUARY и т.д.
  • MonthDay — с месяцем и датой: --06-15
  • Year — с годом: 2026
  • YearMonth — с годом и месяцем: 2026-06
  • ZoneId — с названием часового пояса: Asia/Yekaterinburg
  • ZoneOffset — со смещением часового пояса: +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 для работы с датой и временем.

29 views·2 shares