1. Поняття часового поясу
У світі існує чимало часових поясів: коли у Києві — полудень, у Нью‑Йорку лише ранок, а в Токіо вже вечір. Якщо зберігати дату й час без урахування часового поясу, легко заплутатися: наприклад, коли ваш сервер у Німеччині, а користувач — у Києві, відображення часу "2025-06-01 12:00" означатиме різні речі для кожного.
Часовий пояс — це набір правил, які визначають, скільки часу потрібно додати або відняти від часу за Гринвічем (UTC), щоб отримати місцевий час для конкретного регіону.
У Java для роботи з часовими поясами використовується клас ZoneId. Ось кілька прикладів ідентифікаторів зон:
- "Europe/Kyiv"
- "UTC"
- "America/New_York"
- "Asia/Tokyo"
Навіщо це важливо?
- Коректне відображення часу для користувачів із різних країн.
- Правильний запис часу подій (наприклад, логування, бронювання квитків, граничні терміни).
- Урахування переходу на літній/зимовий час (наприклад, у Європі).
2. ZonedDateTime — дата й час з урахуванням часового поясу
ZonedDateTime — це клас, що зберігає дату, час і інформацію про часовий пояс. Це як LocalDateTime, але також містить зону (регіон), для якої визначено дату й час.
Створення ZonedDateTime
Поточні дата й час у системному часовому поясі
import java.time.ZonedDateTime;
ZonedDateTime now = ZonedDateTime.now();
System.out.println(now); // Наприклад: 2025-06-01T15:30:00+03:00[Europe/Kyiv]
Час у конкретному часовому поясі
import java.time.ZoneId;
ZonedDateTime kyivTime = ZonedDateTime.now(ZoneId.of("Europe/Kyiv"));
ZonedDateTime newYorkTime = ZonedDateTime.now(ZoneId.of("America/New_York"));
System.out.println("Київ: " + KyivTime);
System.out.println("Нью‑Йорк: " + newYorkTime);
Створення з LocalDateTime
import java.time.LocalDateTime;
LocalDateTime meeting = LocalDateTime.of(2025, 6, 1, 18, 0);
ZonedDateTime meetingInKyiv = meeting.atZone(ZoneId.of("Europe/Kyiv"));
System.out.println(meetingInKyiv); // 2025-06-01T18:00+03:00[Europe/Kyiv]
Отримання та встановлення часового поясу
ZoneId tokyoZone = ZoneId.of("Asia/Tokyo");
ZonedDateTime tokyoTime = ZonedDateTime.now(tokyoZone);
System.out.println("Токіо: " + tokyoTime);
Перетворення між часовими поясами: withZoneSameInstant()
Іноді потрібно дізнатися, як одна й та сама подія виглядає в іншому часовому поясі. Для цього використовуйте withZoneSameInstant():
ZonedDateTime kyivMeeting = ZonedDateTime.of(2025, 6, 1, 18, 0, 0, 0, ZoneId.of("Europe/kyiv"));
ZonedDateTime newYorkMeeting = KyivMeeting.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println("Час зустрічі у Києві: " + KyivMeeting);
System.out.println("Та сама подія в Нью‑Йорку: " + newYorkMeeting);
Увага: withZoneSameInstant() переводить час так, щоб він відповідав тому самому моменту в іншому часовому поясі. Якщо використати withZoneSameLocal(), дата й час залишаться тими самими, а пояс зміниться — це майже завжди помилка.
3. Instant — абсолютна точка часу
Instant — це клас, що представляє абсолютний момент часу, незалежно від часового поясу. Технічно — це кількість секунд і наносекунд, що минули від 1 січня 1970 року за Гринвічем (UTC). Якби час мав паспорт, Instant був би його номером.
Створення Instant
import java.time.Instant;
Instant now = Instant.now();
System.out.println(now); // Наприклад: 2025-06-01T12:30:00.123Z
Зверніть увагу на літеру Z — це позначає «Zulu time», тобто UTC.
Створення із секунд від епохи Unix
Instant fromEpoch = Instant.ofEpochSecond(1685616000L);
System.out.println(fromEpoch); // 2023-06-01T00:00:00Z
Перетворення Instant ↔ ZonedDateTime/LocalDateTime
Із ZonedDateTime у Instant
ZonedDateTime zoned = ZonedDateTime.now();
Instant instant = zoned.toInstant();
System.out.println(instant);
З Instant у ZonedDateTime
ZoneId zone = ZoneId.of("Europe/Kyiv");
ZonedDateTime fromInstant = Instant.now().atZone(zone);
System.out.println(fromInstant);
З Instant у LocalDateTime
import java.time.LocalDateTime;
import java.time.Instant;
import java.time.ZoneId;
LocalDateTime local = LocalDateTime.ofInstant(Instant.now(), ZoneId.of("Europe/Kyiv"));
System.out.println(local);
4. Практика: поточний час у різних часових поясах, перетворення між поясами
Отримання поточного часу в різних часових поясах
Зробимо міні‑застосунок, який показує поточний час у Києві, Нью‑Йорку та Токіо:
import java.time.ZonedDateTime;
import java.time.ZoneId;
public class TimeZonesDemo {
public static void main(String[] args) {
ZonedDateTime kyiv = ZonedDateTime.now(ZoneId.of("Europe/Kyiv"));
ZonedDateTime newYork = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime tokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
System.out.println("Київ: " + Kyiv);
System.out.println("Нью‑Йорк: " + newYork);
System.out.println("Токіо: " + tokyo);
}
}
Перетворення часу між поясами
Припустімо, у вас є подія, призначена на 18:00 у Києві. Як дізнатися, яка це буде година в Нью‑Йорку та Токіо?
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class MeetingTime {
public static void main(String[] args) {
LocalDateTime eventTime = LocalDateTime.of(2025, 6, 1, 18, 0);
ZonedDateTime kyivEvent = eventTime.atZone(ZoneId.of("Europe/Kyiv"));
ZonedDateTime newYorkEvent = kyivEvent.withZoneSameInstant(ZoneId.of("America/New_York"));
ZonedDateTime tokyoEvent = kyivEvent.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println("Зустріч у Києві: " + kyivEvent);
System.out.println("У Нью‑Йорку: " + newYorkEvent);
System.out.println("У Токіо: " + tokyoEvent);
}
}
Перетворення LocalDateTime у ZonedDateTime і назад
Local → Zoned:
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
LocalDateTime localTime = LocalDateTime.of(2025, 6, 1, 14, 0);
ZonedDateTime zonedTime = localTime.atZone(ZoneId.of("Europe/Kyiv"));
System.out.println(zonedTime);
Zoned → Local:
LocalDateTime extracted = zonedTime.toLocalDateTime();
System.out.println(extracted);
5. Важливі зауваження й нюанси
Чому не варто зберігати лише LocalDateTime
LocalDateTime — це просто дата й час без часового поясу. Для більшості бізнес‑задач цього недостатньо. Наприклад, якщо ви зберігаєте "2025-06-01 12:00" як LocalDateTime, то для користувача з Києва й користувача з Нью‑Йорка це можуть бути зовсім різні моменти в реальному часі.
Завжди зберігайте абсолютний час (наприклад, Instant) або час із поясом (ZonedDateTime), якщо подія справді привʼязана до конкретного поясу. LocalDateTime підходить лише тоді, коли йдеться про «плаваючі» дати (наприклад, день народження без урахування часу доби й поясу).
Проблеми з переходом на літній/зимовий час
Часові пояси — це не лише зсув відносно UTC, а й правила переходу на літній/зимовий час. У деяких країнах у визначений день час переводять на годину вперед або назад — і якщо ви зберігаєте лише LocalDateTime, то не завжди можна визначити, чи існував такий локальний час.
Приклад «дірки в часі»:
- У США в березні о 2:00 годинники переводять на 3:00.
- Час "2025-03-10 02:30" у Нью‑Йорку не існував.
Працюючи із ZonedDateTime, ви захищені від таких сюрпризів: бібліотека перевіряє коректність часу.
Схема: як повʼязані LocalDateTime, ZonedDateTime, Instant
graph TD
A["LocalDateTime
(дата + час, без зони)"] -->|+ ZoneId| B["ZonedDateTime
(дата + час + зона)"]
B -->|"toInstant()"| C["Instant
(абсолютний час, UTC)"]
C -->|"atZone(ZoneId)"| B
B -->|"toLocalDateTime()"| A
6. Типові помилки під час роботи з ZonedDateTime та Instant
Помилка № 1: використовувати LocalDateTime для глобальних подій.
Якщо ви зберігаєте дату й час зустрічі користувачів із різних країн як LocalDateTime, то кожен побачить свою «12:00», хоча йдеться про різні моменти часу. Для глобальних подій використовуйте ZonedDateTime або Instant.
Помилка № 2: ігнорувати пояс під час розбору рядка.
Якщо ви розбираєте рядок "2025-06-01T12:00:00" без зазначення поясу, вийде LocalDateTime, а не ZonedDateTime. Щоб отримати ZonedDateTime, використовуйте рядки із зазначеним поясом або додавайте його явно.
Помилка № 3: неправильне перетворення між поясами.
Використання withZoneSameLocal() замість withZoneSameInstant() може призвести до неправильного часу. Завжди застосовуйте withZoneSameInstant(), якщо хочете отримати той самий момент часу в іншому поясі.
Помилка № 4: не враховувати перехід на літній/зимовий час.
Якщо плануєте події на межі переходу, обовʼязково використовуйте ZonedDateTime і довіряйте бібліотеці — вона знає про всі переходи й «дірки» в часі.
Помилка № 5: порівнювати ZonedDateTime без урахування поясу.
Два ZonedDateTime з різними поясами, але з однаковим локальним часом, можуть представляти різні моменти часу. Для порівняння використовуйте toInstant().
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ