Java Duration
Работа с временными интервалами (часы, минуты, секунды) в Java с помощью Duration.
Duration представляет длину промежутка времени, измеряемого в секундах и наносекундах. «Два часа». «Пятьсот миллисекунд». «Сорок пять минут». Внутри это два числа — long seconds и int nanos — и все операции над ними являются целочисленной арифметикой. Тип не знает о календарях, месяцах и переходе на летнее время: если указать 24 часа, это означает ровно 86 400 секунд, независимо от того, приходится ли этот промежуток на переход DST.
Это правильный тип для: измерения времени выполнения, таймаутов, повторных попыток с экспоненциальной задержкой, «ожидать не более X секунд» — для любого случая, когда ответом является реальный временной интервал. Сопутствующий тип для календарных длительностей («один месяц», «два года») — Period, который рассматривается в следующей главе.
Создание
Три группы фабричных методов:
Duration.ofNanos(500_000_000);
Duration.ofMillis(500);
Duration.ofSeconds(45);
Duration.ofSeconds(60, 500_000_000); // with nanos
Duration.ofMinutes(2);
Duration.ofHours(1);
Duration.ofDays(7); // exactly 7 * 24 hours
Duration.between(start, end); // from two Temporals
Duration.parse("PT1H30M"); // ISO-8601: PT[hours]H[minutes]M[seconds]SDuration.ofDays(n) — это фабричный метод, который большинство разработчиков используют и большинство из них понимают неправильно. Он производит ровно n * 24 * 3600 секунд. В день перехода на летнее время это не то же самое, что «следующий день в то же время по часам». Для «следующий день» в календарном смысле правильными инструментами являются LocalDate.plusDays(1) или Period.ofDays(1).
Строковый формат — это duration по ISO-8601: PT1H30M45S означает один час, тридцать минут, сорок пять секунд. Префикс PT обязателен (P = Period, T = Time). Для долей секунды используется форма PT0.5S или PT0.000000001S. Duration охватывает всё, что меньше суток; периоды (год, месяц, день) относятся к Period.
Арифметика
d.plus(Duration.ofSeconds(15));
d.plusSeconds(60);
d.plusMinutes(5);
d.plusHours(2);
d.minus(Duration.ofMillis(100));
d.multipliedBy(3);
d.dividedBy(2);
d.negated(); // -d
d.abs();Каждый метод возвращает новый объект Duration (принцип иммутабельности, начиная с LocalDate).
Методы преобразования:
d.toNanos(); // long
d.toMillis(); // long; throws if out of long range
d.toSeconds(); // since Java 9
d.toMinutes();
d.toHours();
d.toDays(); // assumes 24h days
d.getSeconds(); // raw seconds component
d.getNano(); // raw nanos component (0-999_999_999)Методы toX() возвращают единственное значение long — суммарное значение в указанных единицах с усечением. Методы getX() возвращают необработанные компоненты разбивки. Это разные вещи; путаница между ними — самая распространённая ошибка при работе с Duration.
Duration d = Duration.ofSeconds(125);
d.toMinutes(); // 2 (125 / 60)
d.getSeconds(); // 125 (raw)Для форматирования в виде «X минут Y секунд» используйте оба:
long mins = d.toMinutes();
long secs = d.minusMinutes(mins).getSeconds();
System.out.printf("%d:%02d%n", mins, secs); // 2:05Или, начиная с Java 9, специальные вспомогательные методы разбивки:
d.toHoursPart(); // hours within the duration (0-23-ish on positive durations)
d.toMinutesPart(); // minutes within the duration (0-59)
d.toSecondsPart(); // seconds within the duration (0-59)
d.toMillisPart(); // millis within the secondИменно их следует использовать для отображения «1ч 23м 45с» без ручных вычислений по модулю.
Сравнение
d1.isZero(); // d == 0
d1.isNegative(); // d < 0
d1.compareTo(d2);
d1.equals(d2);Duration реализует Comparable<Duration>. Порядок — знаковый: отрицательная длительность меньше нуля, а нуль меньше положительной длительности.
Расстояние между двумя Temporal
Фабричный метод Duration.between(start, end) — тот, который вы будете использовать чаще всего:
Duration d = Duration.between(start, end); // works on Instant, LocalDateTime, ZonedDateTime, LocalTimeОн принимает любую пару значений Temporal, поддерживающих временны́е единицы. LocalDate (только дата) не поддерживает — Duration.between(date1, date2) бросает DateTimeException, поскольку дата не имеет компонента времени. Для календарного расстояния используйте Period.between(date1, date2) или ChronoUnit.DAYS.between(date1, date2).
Соглашение о знаке: положительное значение, если end после start, иначе отрицательное.
Прибавление к другим временным типам
Duration является TemporalAmount, поэтому любой Temporal принимает его через plus/minus:
Instant later = Instant.now().plus(Duration.ofMinutes(30));
LocalDateTime then = LocalDateTime.now().plus(Duration.ofHours(8));Добавление Duration к LocalDate бросает исключение — по той же причине: нет компонента времени. Компилятор это не обнаружит; вызов завершится ошибкой во время выполнения. Если вы хотите именно это, почти наверняка вам нужен Period.
Практический пример: измерение, форматирование, экспоненциальная задержка
Программа ниже измеряет прошедшее время для небольшого фрагмента кода, выводит его в удобочитаемом виде с помощью вспомогательных методов toXxxPart, вычисляет расписание экспоненциальной задержки и демонстрирует границу между Duration и Period, показывая как Duration.between для Instantов, так и Period.between для LocalDateов для одного и того же концептуального временного промежутка.
Что следует вынести из запуска:
- Блок
Thread.sleep(125)измерен какPT0.125-something-S— реальный перерасходsleepобъясняется варьированием на конкретной машине.Duration.between(t0, t1)— правильный вызов: дваInstantа, зона не нужна, точный ответ с разрешением системных часов. - Вспомогательные методы
toXxxPart()дали чистые целые числа: «часть часов», «часть минут», «часть секунд», «часть миллисекунд». Без них пришлось бы вручную делатьtotal / 3600и(total % 3600) / 60. Используйте их всякий раз, когда нужно форматировать длительность для человека. - Цикл экспоненциальной задержки масштабировал длительность с помощью
multipliedBy(2)и выводил каждый результат. Строковое представление ISO-8601 (PT2S,PT4S,PT8S, ...) — это то, что печатаетDuration.toString; оно лаконично и однозначно, идеально для журналов. - Блок «Jan 1 → Apr 1 тремя способами» показал границу между
DurationиPeriod.Period.between(start, end)вернулP3M— три календарных месяца.Duration.betweenдля эквивалентных UTC-инстантов вернул реальное количество времени (90 дней). Оба результата правильны; они отвечают на разные вопросы. Выбирайте тот, чей смысл соответствует намерению вашего кода. - Последний блок попытался вызвать
Duration.between(LocalDate, LocalDate)и получилDateTimeException. Система типов знает, что у даты нет компонента времени, поэтому отказывается вычислять временну́ю длительность. Исправление: либо (1) добавить время (startDate.atStartOfDay(zone)), либо (2) использоватьPeriod.between, если вам нужен именно календарный счёт. Исключение — правильное поведение: молчаливый ответ на этот вопрос был бы вводящим в заблуждение.
Что дальше
Duration — это длина в секундах и наносекундах. Следующая глава, Java Period, — это длина в годах, месяцах и днях, то есть календарный аналог. Эти два типа никогда не смешиваются: Duration не знает о месяцах, Period не знает о часах, и любой код, работающий с обоими, обращается к одному или другому в зависимости от того, должен ли ответ следовать календарю или реальному времени.