LocalTime
java.time.LocalTime represents a time of day (hours, minutes, seconds, and nanoseconds) with no date and no time zone. It answers questions like "what time does the shop open?" or "when is the daily backup?", not "when exactly did this happen?". Like all java.time classes, it is immutable and thread-safe.
Creating a LocalTime
import java.time.LocalTime;
LocalTime now = LocalTime.now(); // current time in the default zone
LocalTime open = LocalTime.of(9, 0); // 09:00
LocalTime precise = LocalTime.of(14, 5, 30); // 14:05:30
LocalTime parsed = LocalTime.parse("18:45"); // ISO format HH:mm[:ss[.nnnnnnnnn]]
LocalTime fromSeconds = LocalTime.ofSecondOfDay(3_600); // 01:00
Hours use the 24-hour clock, from 0 to 23. Values outside the valid ranges are rejected:
LocalTime.of(24, 0); // DateTimeException: Invalid value for HourOfDay (valid values 0 - 23): 24
Constants
LocalTime.MIDNIGHT // 00:00
LocalTime.NOON // 12:00
LocalTime.MIN // 00:00
LocalTime.MAX // 23:59:59.999999999
toString() omits seconds when they are zero, so LocalTime.of(9, 0) prints 09:00 and LocalTime.of(14, 5, 30) prints 14:05:30.
Reading Fields
LocalTime t = LocalTime.of(14, 5, 30);
t.getHour(); // 14
t.getMinute(); // 5
t.getSecond(); // 30
t.getNano(); // 0
t.toSecondOfDay(); // 50730: seconds since midnight
Arithmetic Wraps Around Midnight
A LocalTime lives on a 24-hour clock face, so arithmetic wraps instead of overflowing into another day:
LocalTime.of(23, 30).plusMinutes(45); // 00:15
LocalTime.of(0, 15).minusHours(1); // 23:15
That matches how clocks behave, but the information that a day boundary was crossed is lost. If you need to know the date as well, use LocalDateTime.
The wrap-around also affects loops. A loop such as for (LocalTime t = start; t.isBefore(end); t = t.plusHours(1)) never ends if end is 23:59 and the step jumps past midnight back to 00:00. Loop conditions over LocalTime should be written so that they cannot wrap.
Adjusting and Truncating
LocalTime t = LocalTime.of(14, 5, 30);
t.withMinute(0); // 14:00:30
t.withHour(9); // 09:05:30
t.truncatedTo(ChronoUnit.HOURS); // 14:00
t.truncatedTo(ChronoUnit.MINUTES); // 14:05
Truncating is useful for grouping events by hour or minute, and for removing nanoseconds before display or storage.
Comparing Times
LocalTime a = LocalTime.of(10, 0);
LocalTime b = LocalTime.of(9, 59);
a.isBefore(b); // false
a.isAfter(b); // true
a.compareTo(b) > 0; // true
The comparison is purely within one day: 23:00 is always after 01:00, even if you mean "1 am tomorrow."
Measuring Between Times
ChronoUnit.MINUTES.between(LocalTime.of(9, 15), LocalTime.of(10, 0)); // 45
Duration.between(LocalTime.of(9, 15), LocalTime.of(17, 45)); // PT8H30M
Duration.between(LocalTime.of(22, 0), LocalTime.of(6, 0)); // PT-16H
The last result is negative: from 22:00 back to 06:00 on the same day. For an overnight shift (22:00 to 06:00 the next morning), the correct length is 8 hours, which requires either dates (LocalDateTime) or explicit handling: if the result is negative, add 24 hours.
Duration shift = Duration.between(start, end);
if (shift.isNegative()) shift = shift.plusHours(24); // PT8H
Formatting
DateTimeFormatter twelveHour = DateTimeFormatter.ofPattern("hh:mm a", Locale.ENGLISH);
LocalTime.of(18, 45).format(twelveHour); // "06:45 PM"
Date Formatting has its own lesson.
LocalTime Is Not a Moment
A LocalTime such as 09:00 occurs once every day in every time zone. Storing "the meeting is at 09:00" as a LocalTime alone is fine for recurring rules (the shop opens at 9:00 every day), but not for one specific event, which needs a date and a zone (ZonedDateTime). Combine them when needed:
LocalDate date = LocalDate.of(2026, 11, 20);
LocalDateTime dt = LocalTime.of(9, 0).atDate(date); // 2026-11-20T09:00
ZonedDateTime zdt = dt.atZone(ZoneId.of("Asia/Kolkata")); // a specific moment
Practical Example: Opening Hours and Appointment Slots
A booking page shows whether businesses are open and lists available consultation slots. One business, a pharmacy, is open overnight.
import java.time.*;
import java.util.*;
public class LT {
record Hours(LocalTime open, LocalTime close) {
boolean isOpenAt(LocalTime t) {
if (close.isAfter(open)) {
return !t.isBefore(open) && t.isBefore(close); // same-day hours
}
return !t.isBefore(open) || t.isBefore(close); // overnight hours
}
}
static List<LocalTime> slots(LocalTime from, LocalTime to, Duration length) {
List<LocalTime> out = new ArrayList<>();
for (LocalTime t = from; !t.plus(length).isAfter(to); t = t.plus(length)) out.add(t);
return out;
}
public static void main(String[] args) {
Hours cafe = new Hours(LocalTime.of(8, 0), LocalTime.of(22, 0));
Hours pharmacy = new Hours(LocalTime.of(20, 0), LocalTime.of(6, 0));
for (LocalTime now : List.of(LocalTime.of(7, 30), LocalTime.of(21, 59), LocalTime.of(22, 0), LocalTime.of(2, 15))) {
System.out.printf("%s cafe %-6s pharmacy %s%n", now, cafe.isOpenAt(now) ? "open" : "closed", pharmacy.isOpenAt(now) ? "open" : "closed");
}
System.out.println("Consultation slots: " + slots(LocalTime.of(10, 0), LocalTime.of(12, 0), Duration.ofMinutes(25)));
}
}
Output:
07:30 cafe closed pharmacy closed
21:59 cafe open pharmacy open
22:00 cafe closed pharmacy open
02:15 cafe closed pharmacy open
Consultation slots: [10:00, 10:25, 10:50, 11:15]
isOpenAt treats the opening time as inclusive and the closing time as exclusive, so at exactly 22:00 the cafe counts as closed. For ordinary hours (close after open), the time must lie between them. For overnight hours (close before open), the business is open if the time is after opening or before closing, which correctly reports the pharmacy open at 02:15. slots adds the slot length repeatedly but only keeps slots that end by the closing time: a fifth slot would start at 11:40 and run past 12:00, so it is excluded. Because from and to are both within the same day and to is well before midnight, the loop cannot wrap around.