How to Get the Current Date and Time in Java
Java 12 min read
LocalDate, LocalTime, LocalDateTime, ZonedDateTime and Instant — which one has a time zone, which one is a moment, and why the answer to now() depends on a JVM default you did not set.
java.time offers five ways to ask for the current moment and they are not interchangeable. Two of
them silently depend on the JVM’s default time zone, one has no time zone at all, and only one
represents an unambiguous point on the timeline.
Picking the wrong one produces code that works everywhere the developer runs it and drifts by hours in production. Written against Java 17.
The five types
LocalDate date = LocalDate.now(); // 2026-08-25
LocalTime time = LocalTime.now(); // 14:32:07.481
LocalDateTime dateTime = LocalDateTime.now(); // 2026-08-25T14:32:07.481
ZonedDateTime zoned = ZonedDateTime.now(); // 2026-08-25T14:32:07.481+02:00[Europe/Berlin]
Instant instant = Instant.now(); // 2026-08-25T12:32:07.481Z
| Type | Holds | Time zone | Use for |
|---|---|---|---|
LocalDate | a date | none | a birthday, an invoice date |
LocalTime | a time of day | none | opening hours |
LocalDateTime | both | none | a wall-clock appointment |
ZonedDateTime | both | yes, with rules | a scheduled event in a place |
Instant | a point on the timeline | UTC by definition | a timestamp, an audit log |
The word Local means “no time zone attached”, not “in the local zone”. A LocalDateTime of
2026-08-25T14:00 is 14:00 somewhere unspecified — it names a wall clock, not a moment. Two people in
different countries reading it are not talking about the same instant.
now() reads a default you did not set
Every no-argument now() uses ZoneId.systemDefault(), which comes from the operating system. A
laptop in Berlin and a container running UTC return different answers from the same line of code.
That is the single most common source of “it works on my machine” in date handling, and it is worth making explicit:
LocalDate today = LocalDate.now(ZoneId.of("Europe/Berlin"));
ZonedDateTime nowInTokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
Note that Instant.now() is the exception — it is UTC by definition, so it returns the same value
everywhere.
Use a region id (Europe/Berlin), not an offset (+02:00). The region carries the daylight-saving
rules; the offset is a fixed number that is wrong for half the year. ZoneId.of("UTC") is fine
because UTC has no transitions.
Two related settings are worth pinning deliberately rather than inheriting. The JVM’s zone can be
fixed at startup with -Duser.timezone=UTC, which makes every container behave the same regardless of
the host; and the time-zone database itself ships with the JDK, so a country changing its rules means
a JDK update or a run of the tzupdater tool. A stale database is a rare bug and a confusing one —
correct code producing an offset that was right last year.
System.out.println(ZoneId.systemDefault()); // check what the JVM actually resolved
System.out.println(ZoneId.getAvailableZoneIds().size());
Converting between them
Instant instant = Instant.now();
ZonedDateTime zoned = instant.atZone(ZoneId.of("Europe/Berlin"));
LocalDateTime local = zoned.toLocalDateTime(); // drops the zone
LocalDate date = zoned.toLocalDate();
Instant back = local.atZone(ZoneId.of("Europe/Berlin")).toInstant();
Going from Instant to LocalDateTime loses information and cannot be undone without supplying the
zone again. Going the other way needs a zone because a wall-clock time is not a moment until you say
where.
The awkward case is a LocalDateTime during a daylight-saving transition. On the spring-forward
night, 02:30 does not exist; atZone moves it forward by the gap. On the autumn night it happens
twice; atZone picks the earlier offset. Neither throws, so code that must handle both has to ask:
ZoneId berlin = ZoneId.of("Europe/Berlin");
LocalDateTime ambiguous = LocalDateTime.of(2026, 10, 25, 2, 30);
List<ZoneOffset> offsets = berlin.getRules().getValidOffsets(ambiguous);
// size 0 = the time does not exist; size 2 = it happens twice
Millis, and the clock that can go backwards
long millis = System.currentTimeMillis();
long epochMilli = Instant.now().toEpochMilli();
Both read the wall clock, and a wall clock is adjustable — NTP corrections and manual changes can move it backwards. Measuring an elapsed duration with it can produce a negative number.
For durations, use the monotonic clock:
long start = System.nanoTime();
doWork();
Duration elapsed = Duration.ofNanos(System.nanoTime() - start);
System.nanoTime() has no relationship to any calendar and is only meaningful as a difference
between two readings in the same JVM. Its absolute value is arbitrary — comparing one across two
processes, or persisting it, is meaningless.
The same split exists inside java.time: Clock.systemUTC() reads the wall clock, and there is no
monotonic clock in the API at all. So durations measured for correctness — a cache expiry, a token
lifetime — belong to Instant, and durations measured for observation belong to nanoTime.
Precision is not what the type suggests
Instant can hold nanoseconds. What now() returns depends on the platform clock: on Java 9 and
later it is typically microsecond precision on Linux and coarser on Windows, where it can move in
steps of a millisecond or more.
That matters for two things. Ordering events by timestamp is unreliable when several can share one tick — use a sequence number if order must be total. And a test asserting that two timestamps differ can fail on a machine with a coarse clock.
Databases truncate too. PostgreSQL timestamp keeps microseconds; MySQL DATETIME defaults to
whole seconds unless declared DATETIME(6). A value that round-trips unequal is usually this.
Testing code that reads the clock
Instant.now() inside a method makes that method untestable at a specific moment. Inject a Clock:
public class InvoiceService {
private final Clock clock;
public InvoiceService(Clock clock) {
this.clock = clock;
}
public Invoice create() {
return new Invoice(Instant.now(clock));
}
}
Clock fixed = Clock.fixed(Instant.parse("2026-08-25T12:00:00Z"), ZoneOffset.UTC);
InvoiceService service = new InvoiceService(fixed);
Clock.systemDefaultZone() in production, Clock.fixed(...) in tests. Every now() in java.time
takes an optional Clock, which is what the parameter is for.
The legacy types, and translating out of them
Plenty of code still hands you a java.util.Date or a Calendar. Neither should spread further than
the boundary where it arrives.
Date legacy = new Date();
Instant instant = legacy.toInstant();
Calendar calendar = Calendar.getInstance();
ZonedDateTime zoned = ((GregorianCalendar) calendar).toZonedDateTime();
Date backAgain = Date.from(instant);
java.util.Date is misnamed: it holds no date and no zone, only a millisecond count from the epoch —
it is an Instant with a bad toString(). That toString() renders in the JVM’s default zone, which
is why printing one appears to show a zone it does not store, and why the same object prints
differently on two machines.
Calendar is worse: it is mutable, its month field is zero-based, and Calendar.getInstance() reads
both the default zone and the default locale. Convert on arrival, work in java.time, and convert
back only if an API demands it.
The one legacy type worth keeping is java.sql.Timestamp at the JDBC boundary, and modern drivers
accept Instant and OffsetDateTime directly, so even that is usually avoidable.
Storing and transmitting
Store an instant as UTC — TIMESTAMP WITH TIME ZONE in PostgreSQL, or a DATETIME column that you
have decided is UTC and never write anything else into. Convert to a zone at the point of display,
using the zone of the person reading it rather than the server’s.
Over the wire, ISO-8601 is what toString() already produces:
Instant.now().toString(); // 2026-08-25T12:32:07.481Z
ZonedDateTime.now().toString(); // 2026-08-25T14:32:07.481+02:00[Europe/Berlin]
The bracketed region name is a Java extension, not part of ISO-8601. Other parsers reject it, so send
OffsetDateTime rather than ZonedDateTime across a boundary you do not control.
Related: formatting dates,
comparing them, and
adding or subtracting. The legacy
SimpleDateFormat thread-safety
problem is the main reason to stay in java.time.
Frequently asked questions
What is the difference between LocalDateTime and ZonedDateTime?
LocalDateTime is a wall-clock
reading with no zone, so it is not a moment. ZonedDateTime attaches a region and its
daylight-saving rules, which makes it one.
Does LocalDateTime.now() use my time zone?
It uses the JVM default to read the clock, then discards the zone. The result depends on where the code runs and carries no record of it.
Which type should I store in a database?
Instant, as UTC. Convert to a zone when displaying.
Storing a LocalDateTime means storing a time whose meaning depends on knowledge that is not in the
column.
ZoneId.of(“+02:00”) or ZoneId.of(“Europe/Berlin”)?
The region name. A fixed offset ignores daylight saving and is wrong for part of the year.
Why is System.currentTimeMillis() unsuitable for measuring elapsed time?
It reads an adjustable
wall clock, so an NTP correction can make the difference negative. Use System.nanoTime().
How do I get the current date in a specific time zone?
LocalDate.now(ZoneId.of("Asia/Tokyo")).
Every now() in java.time accepts a ZoneId or a Clock.
Why do two Instants that should be equal differ?
Precision. The platform clock, the database
column and the serialisation format each truncate differently — declare DATETIME(6) on MySQL and
compare with an explicit tolerance if needed.
How do I make code that calls now() testable?
Inject a java.time.Clock and pass it to now().
Clock.fixed in tests pins the moment exactly.
Is Instant always UTC?
Yes — it is a count from the epoch with no zone. That is what makes it the right type for a timestamp and the wrong type for an appointment.
What happens at a daylight-saving transition?
atZone shifts a non-existent time forward by the
gap and picks the earlier offset for a repeated one, without throwing. Use
ZoneRules.getValidOffsets when the difference matters.