Java LocalTime Class
Java LocalTime Class
LocalTime represents a time of day with no date and no time zone attached — just an hour, a minute, a second, and optionally nanoseconds. It is the class for anything that is genuinely a recurring, date-independent time: a store's opening hour, a daily report's cutoff, a scheduled job's run time. Introduced in Java 8 alongside LocalDate, it follows the exact same immutable, method-driven design.
What Is LocalTime?
java.time.LocalTime represents time-of-day on the 24-hour clock, ranging from 00:00 to 23:59:59.999999999, with no association to any particular date or time zone. It implements Comparable<LocalTime>, so times compare and sort in clock order without a custom comparator. Its toString() produces a compact format that omits the seconds field entirely when it is zero — LocalTime.of(14, 30) prints as 14:30, not 14:30:00, which is worth knowing before it surprises you in a log line.
One sentence before the diagram: LocalTime is a closed 24-hour clock face with no concept of a day boundary to advance past.
00:00 (MIDNIGHT)
|
22:00 --+-- 02:00
|
18:00 --+-- 06:00
|
12:00 (NOON)
22:00 .plusHours(5) wraps around to 03:00 -
there is no "next day" for it to roll into
LocalTime compares purely by position on this clock face, not by elapsed time since some fixed reference — a value that looks "earlier" after arithmetic, like 03:00 following 22:00, is exactly what a midnight wraparound produces.
Why LocalTime Was Introduced
Before Java 8, representing a genuinely recurring, date-independent time like "the store opens at 9:00 AM" still meant reaching for Calendar, which forces every value to carry a full date whether one is meaningful or not.
1// File: BeforeLocalTime.java
2import java.util.*;
3
4public class BeforeLocalTime {
5 public static void main(String[] args) {
6 // Representing "store opens at 9:00 AM" with Calendar forces picking
7 // an arbitrary date, even though the date is meaningless here
8 Calendar openingTime = Calendar.getInstance();
9 openingTime.set(Calendar.HOUR_OF_DAY, 9);
10 openingTime.set(Calendar.MINUTE, 0);
11 openingTime.set(Calendar.SECOND, 0);
12
13 System.out.println("Hour: " + openingTime.get(Calendar.HOUR_OF_DAY));
14 System.out.println("Minute: " + openingTime.get(Calendar.MINUTE));
15 }
16}Output:
Hour: 9
Minute: 0
LocalTime drops the meaningless date entirely and represents exactly what the value actually is.
1// File: AfterLocalTime.java
2import java.time.*;
3
4public class AfterLocalTime {
5 public static void main(String[] args) {
6 LocalTime openingTime = LocalTime.of(9, 0);
7
8 System.out.println(openingTime);
9 }
10}Output:
09:00
Both versions represent the same 9:00 AM opening time. Only the second one carries nothing extra that has to be ignored.
Syntax
Creating, reading, and adjusting a LocalTime follows the same method vocabulary as LocalDate, applied to hours, minutes, and seconds instead of years, months, and days.
1// File: LocalTimeSyntaxForms.java
2import java.time.*;
3
4public class LocalTimeSyntaxForms {
5 public static void main(String[] args) {
6 LocalTime simpleTime = LocalTime.of(9, 0);
7 LocalTime preciseTime = LocalTime.of(14, 30, 45);
8 LocalTime parsedTime = LocalTime.parse("18:15:00");
9
10 System.out.println("of(hour, minute): " + simpleTime);
11 System.out.println("of(hour, minute, second): " + preciseTime);
12 System.out.println("parse(): " + parsedTime);
13
14 System.out.println("Hour: " + preciseTime.getHour());
15 System.out.println("Minute: " + preciseTime.getMinute());
16 System.out.println("Second: " + preciseTime.getSecond());
17
18 System.out.println("plusHours(3): " + simpleTime.plusHours(3));
19 System.out.println("plusMinutes(90): " + simpleTime.plusMinutes(90));
20 System.out.println("minusMinutes(15): " + simpleTime.minusMinutes(15));
21
22 System.out.println("isBefore: " + simpleTime.isBefore(preciseTime));
23 System.out.println("MIDNIGHT: " + LocalTime.MIDNIGHT);
24 System.out.println("NOON: " + LocalTime.NOON);
25 }
26}Output:
of(hour, minute): 09:00
of(hour, minute, second): 14:30:45
parse(): 18:15
Hour: 14
Minute: 30
Second: 45
plusHours(3): 12:00
plusMinutes(90): 10:30
minusMinutes(15): 08:45
isBefore: true
MIDNIGHT: 00:00
NOON: 12:00
Notice parse("18:15:00") prints back as 18:15 — the seconds were zero, so toString() drops them, even though the value parsed in with seconds explicitly present.
Common Use Cases
Checking Whether a Time Falls Within a Window
isBefore() and isAfter() combine directly into a range check, with no need to convert either boundary into minutes or a numeric offset first.
1// File: BusinessHoursCheckExample.java
2import java.time.*;
3
4public class BusinessHoursCheckExample {
5 public static void main(String[] args) {
6 LocalTime openingTime = LocalTime.of(9, 0);
7 LocalTime closingTime = LocalTime.of(18, 0);
8
9 LocalTime requestTime = LocalTime.of(19, 30);
10
11 boolean isOpen = !requestTime.isBefore(openingTime) && requestTime.isBefore(closingTime);
12
13 System.out.println("Is store open at " + requestTime + ": " + isOpen);
14 }
15}Output:
Is store open at 19:30: false
Measuring the Gap Between Two Times
Duration.between() is the correct tool for a time-based gap, since LocalTime deals in hours, minutes, and seconds rather than the calendar-based years, months, and days Period models.
1// File: TimeDurationExample.java
2import java.time.*;
3
4public class TimeDurationExample {
5 public static void main(String[] args) {
6 LocalTime shiftStart = LocalTime.of(9, 0);
7 LocalTime shiftEnd = LocalTime.of(17, 30);
8
9 Duration shiftLength = Duration.between(shiftStart, shiftEnd);
10
11 System.out.println("Shift length: " + shiftLength.toHours() + " hours " + shiftLength.toMinutesPart() + " minutes");
12 }
13}Output:
Shift length: 8 hours 30 minutes
Rounding a Time Down to a Coarser Unit
truncatedTo() zeroes out everything below a given unit in one call, instead of chaining several withX(0) calls together.
1// File: TruncationExample.java
2import java.time.*;
3import java.time.temporal.*;
4
5public class TruncationExample {
6 public static void main(String[] args) {
7 LocalTime preciseTime = LocalTime.of(14, 37, 52);
8
9 LocalTime truncatedToMinutes = preciseTime.truncatedTo(ChronoUnit.MINUTES);
10 LocalTime truncatedToHours = preciseTime.truncatedTo(ChronoUnit.HOURS);
11
12 System.out.println("Original: " + preciseTime);
13 System.out.println("Truncated to minutes: " + truncatedToMinutes);
14 System.out.println("Truncated to hours: " + truncatedToHours);
15 }
16}Output:
Original: 14:37:52
Truncated to minutes: 14:37
Truncated to hours: 14:00
Formatting a Time for 12-Hour Display
A DateTimeFormatter pattern with a lowercase hh and an a produces the familiar AM/PM display format most user interfaces actually show.
1// File: TwelveHourFormatExample.java
2import java.time.*;
3import java.time.format.*;
4import java.util.Locale;
5
6public class TwelveHourFormatExample {
7 public static void main(String[] args) {
8 LocalTime meetingTime = LocalTime.of(15, 45);
9
10 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("hh:mm a", Locale.ENGLISH);
11
12 System.out.println(meetingTime.format(formatter));
13 }
14}Output:
03:45 PM
Real-World Example
A food delivery platform needs to decide whether a restaurant can accept a new order right now, based on two separate rules: the restaurant has to actually be open, and the kitchen needs a buffer before closing time to finish orders already in progress. A request arriving five minutes before closing should be rejected even though the restaurant is technically still "open," because the kitchen stopped accepting new work earlier than that.
1// File: RestaurantHours.java
2import java.time.*;
3
4public record RestaurantHours(LocalTime opensAt, LocalTime closesAt, int cutoffMinutesBeforeClose) {}1// File: OrderAcceptanceService.java
2import java.time.*;
3
4public class OrderAcceptanceService {
5
6 public boolean canAcceptOrder(RestaurantHours hours, LocalTime requestTime) {
7 LocalTime lastOrderTime = hours.closesAt().minusMinutes(hours.cutoffMinutesBeforeClose());
8
9 boolean afterOpening = !requestTime.isBefore(hours.opensAt());
10 boolean beforeCutoff = requestTime.isBefore(lastOrderTime);
11
12 return afterOpening && beforeCutoff;
13 }
14}1// File: OrderAcceptanceDemo.java
2import java.time.*;
3
4public class OrderAcceptanceDemo {
5 public static void main(String[] args) {
6 RestaurantHours hours = new RestaurantHours(LocalTime.of(11, 0), LocalTime.of(23, 0), 30);
7
8 OrderAcceptanceService service = new OrderAcceptanceService();
9
10 LocalTime earlyRequest = LocalTime.of(10, 30);
11 LocalTime normalRequest = LocalTime.of(20, 0);
12 LocalTime lateRequest = LocalTime.of(22, 45);
13
14 System.out.println("Order at 10:30 accepted: " + service.canAcceptOrder(hours, earlyRequest));
15 System.out.println("Order at 20:00 accepted: " + service.canAcceptOrder(hours, normalRequest));
16 System.out.println("Order at 22:45 accepted: " + service.canAcceptOrder(hours, lateRequest));
17 }
18}Output:
Order at 10:30 accepted: false
Order at 20:00 accepted: true
Order at 22:45 accepted: false
A mistake that appears often in fresher pull requests is checking only requestTime.isBefore(closesAt) and forgetting the cutoff buffer entirely, which would let an order come in at 22:55 for a kitchen that needed to stop taking orders 30 minutes before close. Computing lastOrderTime explicitly with minusMinutes(), rather than comparing against closesAt directly, is what keeps the kitchen's actual constraint enforced instead of the storefront's advertised closing time.
Combining LocalTime With Other Features
LocalTime combines with LocalDate through LocalDate.atTime(LocalTime) or LocalTime.atDate(LocalDate) to build a LocalDateTime, which is the standard bridge whenever a recurring time — a daily cutoff, for instance — needs to be applied to an actual calendar date. LocalTime pairs with Duration, not Period, for measuring gaps, since a span of hours and minutes is exactly what Duration models. DateTimeFormatter patterns for LocalTime commonly use HH:mm for 24-hour display and hh:mm a for 12-hour display, the same formatter class covered for LocalDate.
Best Practices
Use LocalTime for anything that recurs daily at the same clock time — opening hours, cutoffs, scheduled job times — rather than picking an arbitrary date and using LocalDateTime just to carry a time value that has no real date attached to it.
Combine a LocalTime with an actual LocalDate only at the point an absolute moment is genuinely needed, using atDate() or atTime(). Keep the recurring rule itself date-independent everywhere else in the code.
Reach for truncatedTo() when a time needs rounding down to a coarser unit, rather than manually zeroing fields with a chain of withSecond(0) and withNano(0) calls.
Remember that LocalTime has no concept of "the next day." Arithmetic that crosses midnight wraps around within the same 24-hour clock face rather than rolling into a new date, which matters the moment a calculation spans an overnight boundary.
Common Mistakes
Assuming LocalTime arithmetic that crosses midnight rolls forward into "the next day," the way LocalDateTime would, overlooks that LocalTime has no date component to roll into at all — it simply wraps back around the clock face.
1// File: MidnightWraparoundMistake.java
2import java.time.*;
3
4public class MidnightWraparoundMistake {
5 public static void main(String[] args) {
6 LocalTime lateNightShiftStart = LocalTime.of(22, 0);
7
8 // LocalTime has no concept of "the next day" - adding 5 hours to
9 // 22:00 wraps back around to 03:00 on the SAME 24-hour clock face
10 LocalTime shiftEnd = lateNightShiftStart.plusHours(5);
11
12 System.out.println("Shift start: " + lateNightShiftStart);
13 System.out.println("Shift start + 5 hours: " + shiftEnd);
14 System.out.println("Is shift end 'before' shift start: " + shiftEnd.isBefore(lateNightShiftStart));
15 }
16}Output:
Shift start: 22:00
Shift start + 5 hours: 03:00
Is shift end 'before' shift start: true
shiftEnd.isBefore(shiftStart) reads as true, which looks backwards until you remember LocalTime compares purely by clock position, not by elapsed time since some reference point — an overnight shift genuinely needs a LocalDateTime or a day-crossing flag to model correctly, not a plain LocalTime comparison.
Trying to measure a gap between two LocalTime values with Period.between() does not compile, since Period only accepts LocalDate arguments.
1// File: PeriodOnLocalTimeMistake.java
2import java.time.*;
3
4public class PeriodOnLocalTimeMistake {
5 public static void main(String[] args) {
6 LocalTime start = LocalTime.of(9, 0);
7 LocalTime end = LocalTime.of(17, 30);
8
9 // Period.between(start, end);
10 // This does not compile - Period.between() only accepts LocalDate
11 // arguments, since Period models a date-based amount of time, not a
12 // time-based one
13
14 Duration elapsed = Duration.between(start, end);
15 System.out.println("Elapsed: " + elapsed);
16 }
17}Output:
Elapsed: PT8H30M
Reaching for LocalTime.now() to measure how long an operation actually takes is a subtler mistake that produces plausible-looking but unreliable numbers. now() reads the system's wall clock, which has limited resolution and can be adjusted mid-measurement by an NTP correction or a manual clock change — System.nanoTime() is the tool actually designed for measuring elapsed time, since it is monotonic and immune to wall-clock adjustments.
Interview Questions
Q1. What is LocalTime, and what does it deliberately not track?
LocalTime represents a time of day — hour, minute, second, and optionally nanosecond — with no date and no time zone attached. It deliberately does not track which day the time belongs to or which time zone it should be interpreted in; LocalDateTime and ZonedDateTime add those pieces back in when they are actually needed.
Q2. What happens when you add hours to a LocalTime that pushes it past midnight?
The result wraps back around the 24-hour clock face rather than rolling into a new date, since LocalTime has no date component to advance into. Adding 5 hours to 22:00 produces 03:00, and comparing that result against the original with isBefore() returns true, which looks backwards unless the wraparound behavior is already understood — a detail interviewers at product-based companies particularly like probing, since it causes real bugs in overnight-shift or late-night-cutoff logic.
Q3. Why can't you use Period.between() with two LocalTime values?
Period.between() is declared to accept only LocalDate arguments, because Period specifically models a date-based amount — years, months, and days. A time-based gap measured in hours, minutes, and seconds is exactly what Duration.between() computes instead, and passing LocalTime values to Period.between() is a compile-time type error, not a runtime one.
Q4. How would you combine a LocalTime with a LocalDate to get an absolute moment in time?
Call localDate.atTime(localTime) or localTime.atDate(localDate), either of which produces a LocalDateTime combining both. From there, attaching a ZoneId through atZone() produces a fully-qualified ZonedDateTime if a specific time zone context is also needed.
Q5. What does LocalTime's toString() omit, and why?
It omits the seconds field entirely when it is zero, and the nanosecond field when it is zero, producing the shortest representation that still fully describes the value — 14:30:00 prints as 14:30, while 14:30:15 prints with the seconds included. This trips up anyone expecting a fixed-width format, especially when comparing printed output against a hardcoded expected string in a test.
Q6. Why is LocalTime.now() the wrong tool for measuring how long an operation takes?
LocalTime.now() reads the system's wall clock, which has limited resolution and is not guaranteed to move forward monotonically — an NTP correction or a manual system clock adjustment during a measurement can produce a nonsensical or even negative elapsed time. System.nanoTime() is specifically designed for measuring elapsed time and is immune to wall-clock adjustments, which is why it is the correct tool for benchmarking rather than any java.time now() method.
FAQs
Does LocalTime support nanosecond precision?
Yes. LocalTime stores nanoseconds internally alongside hour, minute, and second, giving it precision down to one nanosecond, though most everyday use cases only ever set or read the hour, minute, and second fields.
Is LocalTime affected by time zones?
No. LocalTime carries no time zone information at all, which is exactly the point — it represents a time as read directly off a clock face, with no assumption about which zone that clock is set to.
What is the difference between LocalTime.MIDNIGHT and LocalTime.MIN?
Both represent exactly the same value, 00:00:00. MIDNIGHT is the semantically named constant most code should reach for when the intent is "the start of the day," while MIN represents the technical minimum value LocalTime can hold, which happens to also be midnight.
Can I compare two LocalTime values with compareTo()?
Yes. LocalTime implements Comparable<LocalTime>, so compareTo() works directly, and it produces the same ordering as chaining isBefore() and isAfter() calls, useful anywhere a Comparator or sorted collection is involved.
How do I round a LocalTime down to the nearest 15 minutes?
There is no single built-in method for an arbitrary interval like 15 minutes — truncatedTo() only supports standard ChronoUnit values like minutes or hours. Rounding to a custom interval typically means converting to total minutes since midnight, rounding that number down to the nearest multiple of 15, and reconstructing a LocalTime from the result.
What happens if I try to create LocalTime.of(24, 0)?
It throws DateTimeException, since the valid hour range for LocalTime is 0 through 23 — there is no hour 24. Midnight at the start of a day is represented as 00:00, not 24:00.
Is LocalTime a good choice for storing a recurring alarm or reminder time?
Yes, precisely because it has no date attached — a daily alarm set for 07:00 is exactly the kind of value LocalTime was designed to represent. The moment that alarm needs to fire on a specific calendar date, combining it with a LocalDate through atDate() produces the concrete LocalDateTime the actual scheduling logic needs.
Summary
LocalTime gives a recurring, date-independent time its own type instead of forcing it to borrow a LocalDateTime or a Calendar just to carry an hour and a minute nobody actually cares about the date for. of() and parse() build one, isBefore() and isAfter() compare them, and Duration.between() — never Period.between() — measures the gap between two of them.
The habit worth carrying forward is respecting the midnight wraparound: LocalTime compares by clock position, not elapsed time since a fixed reference, and any calculation that might cross midnight needs a LocalDateTime or an explicit day-boundary check instead of a bare LocalTime comparison, exactly the trap the overnight shift example walks through. LocalDateTime and ZonedDateTime, covered elsewhere in this series, are what LocalTime combines with the moment an actual date and time zone genuinely matter.
What to Read Next
Learn how to work with a date and time together.