Skip to content
CalliCoder

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
TypeHoldsTime zoneUse for
LocalDatea datenonea birthday, an invoice date
LocalTimea time of daynoneopening hours
LocalDateTimebothnonea wall-clock appointment
ZonedDateTimebothyes, with rulesa scheduled event in a place
Instanta point on the timelineUTC by definitiona 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.