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 rangetoEpochMilli — это потерянное преобразование: наносекунды обрезаются до миллисекунд. Для строк журнала и 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 -> InstantDate внутри является обёрткой вокруг long в миллисекундах от эпохи, поэтому двустороннее преобразование является без потерь с точностью до наносекунд (Date имеет точность до миллисекунды, Instant — до наносекунды). Глава Legacy Date подробно описывает процесс миграции.
Почему всё внутреннее должно быть Instant
Рекомендация, выкристаллизовавшаяся за десять лет использования java.time в промышленных приложениях:
- Внутри используйте
Instant. Хранение, сравнение, журналирование, временные метки сообщений, везде, где значение передаётся между машинами. - На границе — при отображении пользователю, при приёме пользовательского ввода — конвертируйте в
ZonedDateTimeилиLocalDateTime, используя правильную зону для контекста.
Это разделяет «что реально произошло» и «как это помечено». Ошибка на границе (неправильная зона) оставляет внутренние значения корректными; ошибка в системе типов, позволяющая LocalDateTime течь внутренне, как правило, приводит к тому, что временные метки молча оказываются в разных зонах.
Рабочий пример: маленький журнал событий
Приведённая ниже программа записывает последовательность событий в виде Instant-значений, вычисляет длительности между событиями, демонстрирует граничное взаимодействие с календарём путём присвоения зоны для отображения, показывает мост к устаревшему типу Date и, наконец, иллюстрирует правило «без календаря», показывая, что ChronoUnit.MONTHS.between на двух Instant-значениях выбрасывает исключение.
Что следует вынести из запуска:
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 дней». Вместе они позволяют выражать «через час» и «через месяц» без потерь.