1. The concept of a time zone
There are many time zones in the world: when it’s noon in Minsk, it’s only morning in New York, and it’s already evening in Tokyo. If you store date and time without taking the time zone into account, it’s easy to get confusion: for example, if your server is in Germany while the user is in Vladivostok, the time display "2025-06-01 12:00" will mean completely different things for each.
Time zone (timezone) — a rule that defines how much to add to or subtract from Coordinated Universal Time (UTC) to get the “local” time for a specific region.
In Java, the ZoneId class is used to work with time zones. Here are a few examples of zone identifiers:
- "Europe/Minsk"
- "UTC"
- "America/New_York"
- "Asia/Tokyo"
Why does this matter?
- Correct time display for users from different countries.
- Proper recording of event times (for example, logging, ticket booking, deadlines).
- Accounting for daylight saving time transitions (thanks, Europe!).
2. ZonedDateTime — date and time with a time zone
ZonedDateTime is a class that stores the date, time and time zone information. It’s like LocalDateTime, except it also “knows” which region it is in.
Creating ZonedDateTime
Current date and time in the system time zone
import java.time.ZonedDateTime;
ZonedDateTime now = ZonedDateTime.now();
System.out.println(now); // For example: 2025-06-01T15:30:00+03:00[Europe/Minsk]
Time in a specific time zone
import java.time.ZoneId;
ZonedDateTime MinskTime = ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
ZonedDateTime newYorkTime = ZonedDateTime.now(ZoneId.of("America/New_York"));
System.out.println("Minsk: " + MinskTime);
System.out.println("New York: " + newYorkTime);
Creating from LocalDateTime
import java.time.LocalDateTime;
LocalDateTime meeting = LocalDateTime.of(2025, 6, 1, 18, 0);
ZonedDateTime meetingInMinsk = meeting.atZone(ZoneId.of("Europe/Minsk"));
System.out.println(meetingInMinsk); // 2025-06-01T18:00+03:00[Europe/Minsk]
Getting and setting the time zone
ZoneId tokyoZone = ZoneId.of("Asia/Tokyo");
ZonedDateTime tokyoTime = ZonedDateTime.now(tokyoZone);
System.out.println("Tokyo: " + tokyoTime);
Converting between zones: withZoneSameInstant()
Sometimes you need to know how the same event looks in another zone. Use withZoneSameInstant() for this:
ZonedDateTime MinskMeeting = ZonedDateTime.of(2025, 6, 1, 18, 0, 0, 0, ZoneId.of("Europe/Minsk"));
ZonedDateTime newYorkMeeting = MinskMeeting.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println("Meeting time in Minsk: " + MinskMeeting);
System.out.println("The same event in New York: " + newYorkMeeting);
Warning: withZoneSameInstant() converts the time so that it corresponds to the same instant in another zone. If you use withZoneSameLocal(), the date and time will remain the same while the zone changes — that’s almost always a mistake!
3. Instant — an absolute point in time
Instant is a class that represents an absolute moment in time, independent of any time zone. Technically, it’s the number of seconds and nanoseconds since 1 January 1970 in UTC. If time had a passport — Instant would be its number.
Creating Instant
import java.time.Instant;
Instant now = Instant.now();
System.out.println(now); // For example: 2025-06-01T12:30:00.123Z
Note the letter Z — it means “Zulu time”, i.e., UTC.
Creating from Unix epoch seconds
Instant fromEpoch = Instant.ofEpochSecond(1685616000L);
System.out.println(fromEpoch); // 2023-06-01T00:00:00Z
Converting Instant ↔ ZonedDateTime/LocalDateTime
From ZonedDateTime to Instant
ZonedDateTime zoned = ZonedDateTime.now();
Instant instant = zoned.toInstant();
System.out.println(instant);
From Instant to ZonedDateTime
ZoneId zone = ZoneId.of("Europe/Minsk");
ZonedDateTime fromInstant = Instant.now().atZone(zone);
System.out.println(fromInstant);
From Instant to LocalDateTime
import java.time.LocalDateTime;
import java.time.Instant;
import java.time.ZoneId;
LocalDateTime local = LocalDateTime.ofInstant(Instant.now(), ZoneId.of("Europe/Minsk"));
System.out.println(local);
4. Practice: current time in different time zones, converting between zones
Getting the current time in different time zones
Let’s make a small application that shows the current time in Minsk, New York, and Tokyo:
import java.time.ZonedDateTime;
import java.time.ZoneId;
public class TimeZonesDemo {
public static void main(String[] args) {
ZonedDateTime Minsk = ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
ZonedDateTime newYork = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime tokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
System.out.println("Minsk: " + Minsk);
System.out.println("New York: " + newYork);
System.out.println("Tokyo: " + tokyo);
}
}
Converting time between zones
Suppose you have an event scheduled for 18:00 in Minsk. How do you find out what time that will be in New York and Tokyo?
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 minskEvent = eventTime.atZone(ZoneId.of("Europe/Minsk"));
ZonedDateTime newYorkEvent = minskEvent.withZoneSameInstant(ZoneId.of("America/New_York"));
ZonedDateTime tokyoEvent = minskEvent.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println("Meeting in Minsk: " + minskEvent);
System.out.println("In New York: " + newYorkEvent);
System.out.println("In Tokyo: " + tokyoEvent);
}
}
Converting LocalDateTime to ZonedDateTime and back
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/Minsk"));
System.out.println(zonedTime);
Zoned → Local:
LocalDateTime extracted = zonedTime.toLocalDateTime();
System.out.println(extracted);
5. Important notes and caveats
Why you shouldn’t store only LocalDateTime
LocalDateTime is just a date and time without a time zone. For most business logic that’s not enough! For example, if you store "2025-06-01 12:00" as a LocalDateTime, for a user in Minsk and in New York these will be completely different moments in real time.
Always store the absolute time (for example, Instant), or time with a zone (ZonedDateTime) if the event is truly tied to a specific zone. LocalDateTime is good only when you work with “floating” dates (for example, a birthday without regard to time of day and zone).
Issues with daylight saving time transitions
Time zones are not only an offset relative to UTC, but also rules for switching to and from daylight saving time. For example, in some countries on a certain day the time is shifted forward or back by an hour — and if you store only a LocalDateTime, you won’t know whether that time existed at all.
Example of a “gap in time”:
- In the US in March at 2:00 a.m. the clocks are moved to 3:00.
- The time "2025-03-10 02:30" in New York did not exist!
When working with ZonedDateTime, you’re protected from such surprises: the library will validate the time for you.
Diagram: how LocalDateTime, ZonedDateTime, Instant are related
graph TD
A["LocalDateTime
(date + time, no zone)"] -->|+ ZoneId| B["ZonedDateTime
(date + time + zone)"]
B -->|"toInstant()"| C["Instant
(absolute time, UTC)"]
C -->|"atZone(ZoneId)"| B
B -->|"toLocalDateTime()"| A
6. Common mistakes when working with ZonedDateTime and Instant
Error #1: Using LocalDateTime for global events.
If you store the date and time of a meeting between users in different countries as a LocalDateTime, each will see their own “12:00”, even though these are different moments in time. For global events use ZonedDateTime or Instant.
Error #2: Ignoring the time zone when parsing a string.
If you parse the string "2025-06-01T12:00:00" without specifying a zone, you’ll get a LocalDateTime, not a ZonedDateTime. To obtain a ZonedDateTime, use strings that include a zone or add it explicitly.
Error #3: Incorrect conversion between zones.
Using withZoneSameLocal() instead of withZoneSameInstant() can lead to incorrect times. Always use withZoneSameInstant() if you want to get the same instant in another zone.
Error #4: Not accounting for daylight saving time transitions.
If you plan events near transition boundaries, be sure to use ZonedDateTime and trust the library — it knows about all transitions and “gaps” in time.
Error #5: Comparing ZonedDateTime without considering the zone.
Two ZonedDateTime values with different zones but the same local time can represent different instants. For comparison use toInstant().
GO TO FULL VERSION