W3docs

Java Instant

Класс Instant в Java представляет момент на временной шкале в UTC — идеален для временных меток и машинного времени.

Instant — это единственный момент на глобальной временной шкале, хранящийся как наносекунды с начала эпохи Unix (1970-01-01T00:00:00Z). У него нет временной зоны, нет календаря, нет понятия «какой сегодня день» — только счётчик. По своей природе этот счётчик измеряется в UTC, поэтому два значения Instant с разных машин в разных зонах можно сравнивать напрямую: то, у которого меньше число, является более ранним.

Это тип для машинных временных меток. Строки журнала. Временные метки сообщений. «Когда сервер получил запрос». Журналы аудита. Всё, что должно глобально сортироваться и никогда не должно быть неоднозначным с точки зрения того, какой день оно представляет — потому что здесь вообще нет «дня», только секунды.

Создание

Instant now    = Instant.now();                              // current moment, from System clock
Instant epoch  = Instant.EPOCH;                              // 1970-01-01T00:00:00Z
Instant max    = Instant.MAX;                                 // year +1000000000
Instant min    = Instant.MIN;                                 // year -1000000000

Instant fromS   = Instant.ofEpochSecond(1_700_000_000L);
Instant fromMs  = Instant.ofEpochMilli(1_700_000_000_000L);
Instant fromIso = Instant.parse("2025-11-04T19:30:00Z");     // ISO-8601, trailing Z is mandatory

Строковое представление заканчивается буквой Z (от «Zulu time» — военное обозначение UTC). Instant.parse("2025-11-04T19:30:00") (без Z) приводит к ошибке разбора — тип не будет угадывать, какую зону вы имели в виду.

Два фабричных метода, которые вы будете использовать чаще всего:

Instant.ofEpochSecond(epochSec);                              // long seconds, no nanos
Instant.ofEpochSecond(epochSec, nanos);                       // with sub-second resolution

Большинство внешних форматов временных меток (Unix time(2), syslog, JSON-поля created_at в виде целых чисел) представляют собой секунды или миллисекунды с начала эпохи. Фабричные методы ofEpochSecond / ofEpochMilli — стандартный мост к ним.

Разрешение

Instant обладает точностью до наносекунды (1 секунда = 1 000 000 000 нс). На большинстве систем нижележащие часы имеют меньшее разрешение — типично миллисекунду, на современном Linux — микросекунду. Instant.now() возвращает значение с доступным разрешением; неиспользуемые наносекунды равны нулю.

Методы доступа:

long seconds = inst.getEpochSecond();                         // long; can go past 2038
int nanos    = inst.getNano();                                // 0-999_999_999
long milli   = inst.toEpochMilli();                           // throws if out of long range

toEpochMilli — это потерянное преобразование: наносекунды обрезаются до миллисекунд. Для строк журнала и JSON-временных меток это обычно допустимо; для записей событий с высокой частотой используйте getEpochSecond + getNano раздельно.

Без календаря

Instant.getDayOfMonth() не существует. Так же как и getYear, getHour или любые другие методы доступа к календарю. Тип действительно не знает — календарная информация требует временной зоны, а у Instant её нет. Если вы хотите узнать «в какой час это произошло в Нью-Йорке», сначала нужно присвоить зону:

ZonedDateTime zdt = inst.atZone(ZoneId.of("America/New_York"));
int hour = zdt.getHour();
LocalDate date = zdt.toLocalDate();

atZone(zone) — это мост в обратном направлении от ZonedDateTime.toInstant(). Вместе они обеспечивают полный цикл преобразований: момент ↔ метка с зоной. Смотрите раздел Java ZonedDateTime для изучения календарной стороны этой пары.

Арифметика

Та же гибкая форма:

inst.plusSeconds(60);
inst.plusMillis(500);
inst.plusNanos(1_000_000);
inst.plus(Duration.ofMinutes(15));                            // any Duration

inst.minus(Duration.ofDays(1));                                // exactly 24h * 3600s

У Instant нет метода plusDays в календарном смысле. У него есть plus(amount, ChronoUnit), и ChronoUnit.DAYS работает, потому что JDK определяет сутки как ровно 24 часа * 3600 секунд для Instant. Это не то же самое, что календарные сутки при действии перехода на летнее время — именно поэтому Instant не претендует на роль одного из них.

inst.plus(1, ChronoUnit.DAYS);                                // exactly 86_400 seconds
inst.plus(7, ChronoUnit.DAYS);                                // exactly 604_800 seconds

Для операций в форме календаря («на один месяц позже в зоне пользователя») используйте ZonedDateTime:

Instant later = inst.atZone(zone).plusMonths(1).toInstant();

Сравнение

inst1.isBefore(inst2);
inst1.isAfter(inst2);
inst1.equals(inst2);
inst1.compareTo(inst2);

Instant реализует Comparable<Instant> с естественным порядком по секундам эпохи, а затем по наносекундам. equals прост: одинаковые секунды и одинаковые наносекунды.

Расстояние

Duration d = Duration.between(start, end);                     // a Duration
long millis = ChronoUnit.MILLIS.between(start, end);
long days = ChronoUnit.DAYS.between(start, end);               // 24h-equivalent days

Для машинных временных меток всё это точно — нет никакой неоднозначности календаря. ChronoUnit.MONTHS.between(start, end) для значений Instant выбрасывает исключение, потому что месяцы не имеют постоянной длины в секундах: зоны нет, и вычислитель не может определить, какой именно месяц содержит эти секунды. Это правильное поведение при ошибке.

Мост к java.util.Date

Старый код использует java.util.Date. Преобразования прямые:

Date legacy = Date.from(inst);                                 // Instant -> Date
Instant back = legacy.toInstant();                              // Date -> Instant

Date внутри является обёрткой вокруг long в миллисекундах от эпохи, поэтому двустороннее преобразование является без потерь с точностью до наносекунд (Date имеет точность до миллисекунды, Instant — до наносекунды). Глава Legacy Date подробно описывает процесс миграции.

Почему всё внутреннее должно быть Instant

Рекомендация, выкристаллизовавшаяся за десять лет использования java.time в промышленных приложениях:

  • Внутри используйте Instant. Хранение, сравнение, журналирование, временные метки сообщений, везде, где значение передаётся между машинами.
  • На границе — при отображении пользователю, при приёме пользовательского ввода — конвертируйте в ZonedDateTime или LocalDateTime, используя правильную зону для контекста.

Это разделяет «что реально произошло» и «как это помечено». Ошибка на границе (неправильная зона) оставляет внутренние значения корректными; ошибка в системе типов, позволяющая LocalDateTime течь внутренне, как правило, приводит к тому, что временные метки молча оказываются в разных зонах.

Рабочий пример: маленький журнал событий

Приведённая ниже программа записывает последовательность событий в виде Instant-значений, вычисляет длительности между событиями, демонстрирует граничное взаимодействие с календарём путём присвоения зоны для отображения, показывает мост к устаревшему типу Date и, наконец, иллюстрирует правило «без календаря», показывая, что ChronoUnit.MONTHS.between на двух Instant-значениях выбрасывает исключение.

java— editable, runs on the server

Что следует вынести из запуска:

  • Instant.parse("2025-11-04T19:30:00Z") разобрался только благодаря завершающей Z. Уберите Z — и разбор завершится ошибкой: тип настаивает на том, чтобы знать, что строка является UTC. Другие зоны должны передаваться через ZonedDateTime.parse (или задаваться через atZone).
  • Последовательность событий использовала Duration.between(...). Каждый результат являлся чистым целочисленным количеством миллисекунд — без путаницы с зонами, без перехода на летнее время, без календарной арифметики. Именно поэтому серверное время должно быть в Instant: арифметика — это просто вычитание long-значений под капотом.
  • Блок «тот же момент, две метки зон» вывел ZonedDateTime-значения, которые выглядели по-разному, но являлись одним и тем же Instant. atZone(...) — это сугубо операция отображения для Instant. Если вы хотите выполнить календарные вычисления (следующий месяц, конец недели), делайте это с ZonedDateTime, а затем вызовите .toInstant(), чтобы вернуться обратно.
  • Date.from(inst) и legacy.toInstant() были преобразованиями без потерь с точностью до наносекунд. Date хранит только миллисекунды, поэтому при преобразовании через Date точность ниже миллисекунды теряется. Для большинства журналов это допустимо; для захвата событий с высокой точностью оставайтесь в Instant от начала до конца и никогда не делайте цикл через Date.
  • ChronoUnit.MONTHS.between(a, b) выбросил UnsupportedTemporalTypeException. Это правильное поведение при ошибке: месяцы не имеют постоянной длины в секундах, и JDK отказывается изобретать ответ. Переход через ZonedDateTime предоставил недостающую зону, и тот же вызов сработал. Паттерн универсален: операции в форме календаря требуют зоны, и система типов заставляет вас указывать её явно.

Что дальше

Instant — это момент. Следующие две главы рассматривают длительности времени между моментами: Java Duration для измерений «X секунд, Y наносекунд» и Java Period для календарных длительностей «X лет, Y месяцев, Z дней». Вместе они позволяют выражать «через час» и «через месяц» без потерь.

Практика

Практика
В строке журнала есть поле `created_at: 1700000000` (секунды в формате Unix). Вам нужно значение `java.time`, которое можно сравнить с `Instant.now()` и передать в вычисление `Duration.between(...)`. Какое преобразование верно?
В строке журнала есть поле `created_at: 1700000000` (секунды в формате Unix). Вам нужно значение `java.time`, которое можно сравнить с `Instant.now()` и передать в вычисление `Duration.between(...)`. Какое преобразование верно?
Was this page helpful?