☕ Java

LocalTime

LocalTime is the java.time class representing a time-of-day without any date or time zone attached — just an hour, minute, second, and nanosecond on a 24-hour clock. This entry covers what LocalTime intentionally omits and why, how to create and parse LocalTime values with varying precision, how arithmetic on LocalTime wraps around midnight instead of rolling into a different day, and how LocalTime is meant to be combined with LocalDate (via LocalDateTime) rather than used as a date substitute.

What LocalTime Represents — Time-of-Day, Nothing Else

LocalTime stores exactly an hour-of-day (0-23), a minute (0-59), a second (0-59), and a nanosecond (0-999,999,999), and nothing about which date or which time zone that time-of-day belongs to — it answers the question "what time does the clock read" in total isolation from "on what day" and "in what location," which is exactly the same narrowing philosophy LocalDate applies to dates, applied symmetrically to times. This makes LocalTime the correct type for recurring or date-independent time-of-day concepts — a store's opening time (09:00), a daily alarm, a recurring meeting's start time — situations where the same LocalTime value is meant to apply on many different calendar dates, rather than pinning down one specific moment in history. Critically, LocalTime should not be reached for when what's actually needed is a duration or elapsed amount of time — "how long did the task take" is a Duration (or Period, for date-based spans), not a LocalTime, since a LocalTime value is always a position on the 24-hour clock face, never a length of time. Constructing LocalTime via its factory methods, of(hour, minute) and its overloads accepting additional second and nanosecond arguments, is the most direct route, while LocalTime.now() reads the current time-of-day from the system clock in the default time zone (with a LocalTime.now(ZoneId) overload for other zones), and like LocalDate, LocalTime has no public constructor and is fully immutable, with every apparent mutation returning a new instance.
Java
// ── LocalTime — purely a position on the 24-hour clock, no date attached
LocalTime openingTime = LocalTime.of(9, 0);          // 09:00 — hour, minute only
LocalTime preciseTime = LocalTime.of(14, 30, 15, 500_000_000);  // 14:30:15.500

// LocalTime is meant for RECURRING times — the same value, many different dates:
LocalTime storeOpens = LocalTime.of(9, 0);
// applies equally to today, tomorrow, and every day the store is open

// ── now() — reads the current time-of-day, zone-dependent like LocalDate.now()
LocalTime currentTime = LocalTime.now();
LocalTime currentInTokyo = LocalTime.now(ZoneId.of("Asia/Tokyo"));

// ── NOT for elapsed time/duration — that's a different job entirely ──
// LocalTime taskLength = ???   // WRONG — a length of time is not a clock position
Duration taskLength = Duration.ofMinutes(45);   // CORRECT type for "how long"

Parsing, Formatting, and Precision

LocalTime.parse(text) defaults to expecting ISO-8601 time format (HH:mm:ss with optional fractional seconds, e.g. "14:30:15" or "14:30:15.123"), and like LocalDate, supports an overload accepting a custom DateTimeFormatter for any other textual layout, with the same formatter reused in the opposite direction via localTime.format(formatter) to produce custom-formatted output strings — DateTimeFormatter.ofPattern("hh:mm a") for a 12-hour clock with an AM/PM marker is a common example for user-facing display. A detail that trips up newcomers is that LocalTime.of(hour, minute) (without explicit seconds or nanoseconds) silently defaults those unspecified fields to zero rather than failing or carrying over any other value — LocalTime.of(9, 0) is exactly equivalent to LocalTime.of(9, 0, 0, 0), and two LocalTime values built with different levels of precision (one specifying nanoseconds, one not) are only equal if every field, including the implicitly-zeroed ones, actually matches.
Java
// ── Parsing — ISO-8601 by default, custom formatter otherwise ────────
LocalTime parsed = LocalTime.parse("14:30:15");          // default format
LocalTime parsedFraction = LocalTime.parse("14:30:15.250");  // fractional seconds OK

DateTimeFormatter twelveHour = DateTimeFormatter.ofPattern("hh:mm a");
LocalTime fromTwelveHour = LocalTime.parse("02:30 PM", twelveHour);

// ── Formatting back out, using the same formatter ─────────────────────
String display = LocalTime.of(14, 30).format(twelveHour);   // "02:30 PM"

// ── Unspecified fields default to ZERO, not "unset" ───────────────────
LocalTime short1 = LocalTime.of(9, 0);              // implicitly 09:00:00.000000000
LocalTime explicit = LocalTime.of(9, 0, 0, 0);
System.out.println(short1.equals(explicit));   // true — same underlying fields

Arithmetic That Wraps Around Midnight

Because LocalTime represents only a position on a 24-hour clock face with no day attached, arithmetic that crosses midnight wraps back around to 00:00 rather than rolling forward into "the next day" — there is no next day for a LocalTime to roll into, since it has no date field to increment in the first place. LocalTime.of(23, 0).plusHours(2) produces 01:00, not "tomorrow at 01:00," because tomorrow is not a concept LocalTime is capable of expressing; if the actual intent is to track a specific moment that does cross a day boundary, LocalDateTime (or a higher-level type like Instant/ZonedDateTime) is the correct type to reach for instead, since only those types have a date component available to actually increment. This wraparound behavior also affects how durations between two LocalTime values are computed: Duration.between(start, end) on two plain LocalTime values that appear to span midnight (e.g., start = 23:00, end = 01:00) computes a negative duration, since LocalTime has no information telling it that "01:00" refers to the next day rather than treating both values as positions within the very same, single 24-hour cycle — getting a correct elapsed-time calculation across midnight specifically requires combining each LocalTime with an actual date (producing two LocalDateTime values) before measuring the Duration between them.
Java
// ── plusHours/minusHours wrap around midnight — there's no "next day"
LocalTime late = LocalTime.of(23, 0);
LocalTime wrapped = late.plusHours(2);
System.out.println(wrapped);   // 01:00 — NOT "tomorrow 01:00", just wraps to 01:00

// ── Duration.between on plain LocalTime across midnight is WRONG/negative
LocalTime start = LocalTime.of(23, 0);
LocalTime end = LocalTime.of(1, 0);
Duration wrongDuration = Duration.between(start, end);
System.out.println(wrongDuration.toMinutes());   // -1320 — treated as SAME day, goes backward!

// ── CORRECT — attach actual dates first, then measure across the boundary
LocalDateTime startDateTime = LocalDateTime.of(2024, 6, 15, 23, 0);
LocalDateTime endDateTime = LocalDateTime.of(2024, 6, 16, 1, 0);   // explicitly next day
Duration correctDuration = Duration.between(startDateTime, endDateTime);
System.out.println(correctDuration.toMinutes());   // 120 — correct, two hours elapsed

Related Topics in Date Time API

LocalDate
LocalDate is the java.time class (introduced in Java 8 as part of the redesigned date-time API replacing the deprecated java.util.Date/Calendar) representing a date without a time component or a time zone — just a year, month, and day on the ISO-8601 calendar. This entry covers why LocalDate deliberately carries no time-of-day or zone information, how it is created and parsed, how it is manipulated via its immutable, fluent with*/plus*/minus* methods, how two LocalDate instances are compared and the period between them measured, and the common pitfalls of treating it as if it secretly carried a time or zone after all.
LocalDateTime
LocalDateTime is the java.time class combining a LocalDate and a LocalTime into a single date-and-time-of-day value that still, deliberately, carries no time zone or UTC-offset information. This entry covers why LocalDateTime is best understood as a composite of the two simpler types rather than a primitive type in its own right, how it is created from and decomposed back into its LocalDate/LocalTime parts, why the absence of a zone makes it unsuitable for representing one unambiguous, real-world instant, and how it relates to and converts into the zone-aware types (ZonedDateTime, Instant) when an actual moment in time is what's needed.
ZonedDateTime
ZonedDateTime is the java.time class representing a LocalDateTime combined with a ZoneId, fully resolved against the time zone's rules to also carry a specific UTC offset — making it the type capable of representing one genuinely unambiguous real-world instant while still preserving human-readable, zone-local wall-clock fields. This entry covers the relationship between ZoneId and the resolved offset, how daylight saving transitions create gaps and overlaps that ZonedDateTime must resolve, how zone-aware arithmetic differs from plain LocalDateTime arithmetic across those transitions, and when to prefer ZonedDateTime over the simpler offset-only OffsetDateTime or the zone-independent Instant.
Period
Period is the java.time class representing a date-based amount of time expressed in years, months, and days — a human-calendar quantity like '2 years, 3 months, and 10 days' rather than a fixed number of elapsed seconds. This entry covers how Period differs fundamentally from Duration in what unit of measurement it operates in, how Period.between() computes a breakdown rather than a single total, why adding a Period to a date is calendar-aware rather than fixed-length, and the common pitfall of expecting a Period's individual fields to sum to a meaningful total without normalization.