LocalTime is the java.time class for time-of-day values with no date and no time zone. It stores an hour, minute, second, and nanosecond, which fits modeling store opening hours, the start of a flash sale, or a daily inventory job that runs at 02:00. The previous lesson covered LocalDate (year-month-day). This one covers the wall-clock part of the picture.
A LocalTime is a point on a 24-hour clock. It does not know what day it is, and it does not know what time zone the clock is set to. LocalTime.of(9, 30) for a store that opens at 9:30 AM is just "the moment when a clock reads 09:30." Whether that means 09:30 in Mumbai, New York, or anywhere else is a separate concern that ZonedDateTime handles.
The internal layout is four fields:
| Field | Range | Notes |
|---|---|---|
| Hour | 0 to 23 | 24-hour clock. There is no 24 and no -1. |
| Minute | 0 to 59 | |
| Second | 0 to 59 | No leap second 60. |
| Nanosecond | 0 to 999,999,999 | Sub-second precision. |
That gives a resolution of one nanosecond and a range that wraps cleanly at midnight. The class is immutable and thread-safe, so every method that "changes" a value actually returns a new LocalTime.
The diagram shows the cycle a LocalTime lives on. MIN is 00:00, MAX is 23:59:59.999999999, and arithmetic wraps around at midnight in both directions. The wrap behavior comes up again in the plusHours and minusMinutes section below.
There are three common ways to get a LocalTime: ask for the current wall-clock time, construct one from fields, or parse one from a string.
LocalTime.now() reads the system clock and returns the current time of day in the default zone.
The exact output depends on when the program runs. The printed form is ISO-8601: hour, minute, second, and a fractional part if the nanos are non-zero. Trailing zero nanos are trimmed, so a value of 09:30:00.000000000 prints as 09:30.
LocalTime.of(...) has four overloads, one per level of precision.
Each overload adds one more field. Passing an out-of-range value throws DateTimeException at construction time, which is one of the main advantages over the old Date and Calendar classes that rolled bad input over.
LocalTime.parse accepts any ISO-8601 time. The seconds and fractional seconds are optional.
Custom formats like "9:30 AM" are not handled by LocalTime.parse directly; those require a DateTimeFormatter. The default parse method only understands ISO-8601.
LocalTime ships four named constants that come up often:
| Constant | Value | Use For |
|---|---|---|
LocalTime.MIN | 00:00 | Start-of-day boundary checks. |
LocalTime.MAX | 23:59:59.999999999 | End-of-day boundary checks. |
LocalTime.MIDNIGHT | 00:00 | Same as MIN, but reads better in some contexts. |
LocalTime.NOON | 12:00 | Reads better than LocalTime.of(12, 0). |
MIN and MIDNIGHT are the same value with two names. Use whichever reads better in context: MIDNIGHT for "the start of a new day," MIN for "the smallest valid LocalTime."
The getters mirror the four fields:
getHour returns an int in [0, 23], getMinute and getSecond return int in [0, 59], and getNano returns an int in [0, 999_999_999]. There is no getMillisecond because the underlying storage is nanos; for milliseconds, divide nanos by one million.
Every plus* and minus* method returns a new LocalTime. The original is untouched.
The interesting case is what happens when arithmetic crosses midnight. LocalTime has no concept of "the next day," so the value wraps around. Adding three hours to 23:00 gives 02:00, not 26:00.
That wrap is a feature, not a bug, but it is also a sharp edge. An inventory cron starting at 02:00 with "run it three hours earlier" produces 23:00 on the same LocalTime, with no information about which day. When the day matters, combine LocalTime with a LocalDate into a LocalDateTime.
plusHours, plusMinutes, and friends allocate a new LocalTime every call. For one-off calls that does not matter. Inside a tight loop that builds thousands of times per second, compute the offset once and reuse it.
The available arithmetic methods cover all four fields:
| Add | Subtract | Effect |
|---|---|---|
plusHours(long) | minusHours(long) | Adds or subtracts hours, wraps at midnight. |
plusMinutes(long) | minusMinutes(long) | Same, by minutes. |
plusSeconds(long) | minusSeconds(long) | Same, by seconds. |
plusNanos(long) | minusNanos(long) | Same, by nanoseconds. |
There is no plusMillis on LocalTime. For a long of milliseconds, multiply by one million and call plusNanos, or use the more general plus(amount, unit) overload with ChronoUnit.MILLIS.
The with* methods return a copy with one field changed. Like plus*, they never mutate the original.
Out-of-range arguments throw DateTimeException. withHour(24) fails the same way LocalTime.of(24, 0) does.
LocalTime implements Comparable<LocalTime> and provides three convenience methods:
| Method | Returns | Use For |
|---|---|---|
isBefore(other) | boolean | Strict less-than. |
isAfter(other) | boolean | Strict greater-than. |
compareTo(other) | int | Negative, zero, or positive. |
equals(other) | boolean | All four fields match. |
The classic use case is checking whether the store is open right now.
The !now.isBefore(opening) form is the standard way to say "now is at or after opening," because LocalTime has no isBeforeOrEqual method. The other half, now.isBefore(closing), uses strict less-than so that 21:00 itself counts as closed.
Equality compares every field, including nanos. Two values that print the same can still be unequal if their nanoseconds differ:
Both values print as 09:30 when their nanos are zero, but the one with a single non-zero nano prints with the full fractional second. Times parsed from outside sources can carry nanos that break what looks like an obvious equality.
A LocalTime parsed from a system source often carries more precision than is needed. truncatedTo(unit) zeros out everything below the given unit.
Truncation fits grouping orders into time buckets ("how many orders came in during the 14:00 hour?") or for comparing two times at minute granularity without nanosecond noise. It reads better than withSecond(0).withNano(0).
Truncation is O(1) and allocates one new LocalTime. For bucketing millions of events by minute, this is the cheap operation, not the expensive one.
Truncation only works for units smaller than or equal to a day. ChronoUnit.DAYS is the upper bound; passing ChronoUnit.WEEKS throws UnsupportedTemporalTypeException.
LocalTime stores nanoseconds, not milliseconds. That matters when mixing it with older APIs that only offer milliseconds, like System.currentTimeMillis(). The conversion is straightforward but easy to get wrong.
The toString format trims trailing zeros in the fractional part, so half a second prints as .500 and a single nanosecond prints as the full nine-digit form. The underlying storage is the same: one int field for nanos in the range [0, 999_999_999].
Two corner cases come up enough to call out. LocalTime does not accept 24:00 as a representation of midnight, and it does not accept second 60 for leap seconds.
The hour rule means midnight is always 00:00. The "end of day" representation 24:00 from some other systems is not valid here; LocalTime.MAX is 23:59:59.999999999. The second rule means LocalTime does not model leap seconds. UTC has had 27 of them since 1972, but the Java time-line treats every minute as 60 seconds. Leap-second awareness requires a different abstraction; for almost every business application, this isn't a concern.
For quick "how far apart are these two times?" calculations, ChronoUnit.between(a, b) returns a long in the requested unit. It treats both times as points on the same 24-hour clock and returns the signed gap.
ChronoUnit.MINUTES.between truncates toward zero. The actual gap is 32 minutes and 30 seconds, so the minute count is 32, not 33. For a richer object that knows about hours and minutes together, Duration.between(a, b) fits better. For now, ChronoUnit.between is enough for SLA checks and similar one-shot math.
One detail: ChronoUnit.between on two LocalTime values assumes they are on the same day. If ticketRaised is 23:30 and firstReply is 00:15, the result is -1395 minutes, not 45. That is consistent with the wrap-around behavior of arithmetic. To span midnight, use LocalDateTime.
A LocalTime on its own answers "what does the clock read?" but not "on which day?" For both, LocalTime has an atDate(LocalDate) method that returns a LocalDateTime.
10 quizzes