LocalDate, LocalTime and LocalDateTime are the main classes of the java.time package that provide date and time handling without timezone binding. According to Oracle documentation (Java 17, 2024), these types are designed as immutable and thread-safe, making them safe for multithreaded applications. They became available on Android through desugaring starting from API 26, and for older versions — through the ThreeTenABP library.
Key Takeaways
LocalDate — a class that represents a date in year-month-day format without time and timezone information. It is used to store data such as birthdays, event dates, or expiration dates.
LocalDate stores a year in the range from -999999999 to +999999999, a month from 1 to 12, and a day of the month considering leap years. The class is completely immutable — any operation returns a new object.
LocalTime represents the time of day: hours, minutes, seconds, and nanoseconds. Maximum precision is up to a nanosecond. LocalTime contains no date or timezone information, making it convenient for storing store opening hours or process duration.
LocalDateTime combines LocalDate and LocalTime into a single object. This is the most commonly used type when you need to store both date and time, but timezone binding is not required. For example, the date and time of a concert in local format.
According to Oracle Java Documentation (2024), all three classes are designed based on ideas from the Joda-Time library but with improved architecture and full integration into the standard library.
The java.time package was introduced in Java 8 as a replacement for the outdated Date, Calendar, and SimpleDateFormat classes. Its architecture is built on the principles of immutable objects and a fluent interface.
A key feature is that all main classes are value-based. This means their instances are compared by value, not by reference, and they cannot be inherited. To compare two objects, use the equals method, not the == operator.
The package is divided into several categories. Types without timezone — LocalDate, LocalTime, LocalDateTime — are used for local dates and times. Types with timezone — ZonedDateTime, OffsetDateTime, OffsetTime — add information about offset or zone. Instant types — Instant — represent a point on the timeline in UTC.
This separation solves a problem inherent in the old API: a developer never knew whether a Date object contained timezone information or not. In java.time, each type explicitly declares its semantics.
The LocalDate class provides many methods for creating, reading, and modifying dates. The current date can be obtained via the static method now(). A specific date — via the of(int year, int month, int dayOfMonth) method.
Getters are used to read date components: getYear(), getMonthValue(), getDayOfMonth(), getDayOfWeek(), getDayOfYear(). The getMonth() method returns the Month enum, and getDayOfWeek() returns the DayOfWeek enum.
LocalDate supports date checking. The isBefore(), isAfter(), and isEqual() methods allow comparing dates. The isLeapYear() method checks whether the year is a leap year. The lengthOfMonth() method returns the number of days in the month, and lengthOfYear() returns the number of days in the year.
For modification, use withYear(), withMonth(), withDayOfMonth() methods, which return a new object with the changed component. The plusDays(), minusMonths() and similar methods perform date arithmetic.
LocalTime represents the time of day with nanosecond precision. The standard format is ISO-8601 (HH:mm:ss.nnnnnnnnn). The minimum value is 00:00, the maximum is 23:59:59.999999999.
You can create a LocalTime object using now() for the current time, of(int hour, int minute), of(int hour, int minute, int second), or of(int hour, int minute, int second, int nanoOfSecond). The parse(CharSequence text) method parses a string in ISO-8601 format.
Getters include getHour(), getMinute(), getSecond(), getNano(). The toSecondOfDay() method returns the number of seconds since the start of the day, and toNanoOfDay() returns nanoseconds. This is convenient for calculating duration within a single day.
LocalTime supports the same comparison and modification operations as LocalDate: plusHours(), minusMinutes(), withHour(), withMinute(). The isBefore() and isAfter() methods work considering that time is cyclical within a day.
LocalDateTime combines the capabilities of LocalDate and LocalTime into a single class. It stores both date and time, but without timezone. This is the most flexible local type, but it requires caution when used in distributed systems.
You can create LocalDateTime using the static methods now(), of(LocalDate date, LocalTime time), of(int year, Month month, int dayOfMonth, int hour, int minute) and their overloads. You can also combine LocalDate and LocalTime using the atTime() method.
LocalDateTime provides access to all date and time fields through corresponding getters: toLocalDate() and toLocalTime() return individual components. The truncatedTo(TemporalUnit unit) method allows rounding time to a given precision — for example, to minutes.
To convert to a timezone, use the atZone(ZoneId zone) method, which returns ZonedDateTime. This is the only way to add a timezone to LocalDateTime.
All three classes use a unified creation pattern through static factory methods. Class constructors are declared as private — you cannot create an object directly using new.
Main creation methods:
The of method has many overloads. For LocalDate you need year, month, and day. For LocalTime — hours and minutes (optionally seconds and nanoseconds). For LocalDateTime — year, month, day, hours, minutes. The month can be passed as an int (1-12) or as the Month enum.
val today = LocalDate.now()
val specificDate = LocalDate.of(2026, Month.JULY, 21)
val parsedDate = LocalDate.parse("2026-07-21")
val currentTime = LocalTime.now()
val lunchTime = LocalTime.of(13, 30, 0)
val parsedTime = LocalTime.parse("13:30:00")
val now = LocalDateTime.now()
val meeting = LocalDateTime.of(2026, 7, 21, 15, 0)
The java.time classes are designed for convenient conversion between each other. LocalDate can be converted to LocalDateTime via the atTime(LocalTime) or atStartOfDay() method. LocalTime — via atDate(LocalDate).
LocalDateTime can be converted back to LocalDate via toLocalDate() and to LocalTime via toLocalTime(). To convert to ZonedDateTime, use the atZone(ZoneId) method.
Conversion to java.util.Date (for compatibility with legacy code) requires an intermediate step through Instant and a timezone. According to Baeldung (2024), this operation is performed via Date.from(instant).
val date = LocalDate.of(2026, 7, 21)
val dateTime = date.atTime(LocalTime.of(10, 30))
val time = LocalTime.of(14, 0)
val dateTimeFromTime = time.atDate(date)
val extractedDate = dateTime.toLocalDate()
val extractedTime = dateTime.toLocalTime()
val zoned = dateTime.atZone(ZoneId.of("Europe/Moscow"))
For formatting and parsing, the DateTimeFormatter class is used. It provides predefined formats through constants (ISO_LOCAL_DATE, ISO_LOCAL_TIME, ISO_LOCAL_DATE_TIME) and the ability to create custom ones using pattern strings.
Formatting patterns use symbols: yyyy — year, MM — month (two-digit), dd — day, HH — hour (0-23), mm — minute, ss — second. The format() method is called on the date-time object or through DateTimeFormatter.
DateTimeFormatter also supports localization through the static methods ofLocalizedDate(FormatStyle), ofLocalizedTime(FormatStyle), and ofLocalizedDateTime(FormatStyle). Available styles are SHORT, MEDIUM, LONG, and FULL.
val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")
val formatted = LocalDateTime.now().format(formatter)
val parsed = LocalDate.parse(
"21.07.2026",
DateTimeFormatter.ofPattern("dd.MM.yyyy")
)
All three classes implement the Comparable interface, allowing them to be compared naturally. The compareTo() method returns a negative number, zero, or a positive number depending on the order. The isBefore(), isAfter(), and isEqual() methods return a boolean.
For LocalDate, comparison is chronological — an earlier date is considered smaller. For LocalTime — by time of day. For LocalDateTime — first by date, then by time. All comparisons correctly account for leap years and the number of days in months.
An important difference from the old API: equals() for LocalDate, LocalTime, and LocalDateTime compares values, not references. This means two objects with the same fields will be equal, even if they are different instances.
val d1 = LocalDate.of(2026, 7, 21)
val d2 = LocalDate.of(2026, 12, 25)
if (d1.isBefore(d2)) {
Log.d("Date", "d1 is before d2")
}
val sortedDates = listOf(d2, d1).sorted()
All three classes support arithmetic operations through the plus and minus methods. For LocalDate, plusDays(), plusWeeks(), plusMonths(), plusYears() and the corresponding minus methods are available. LocalTime supports plusHours(), plusMinutes(), plusSeconds(), plusNanos().
LocalDateTime inherits all arithmetic operations from both types. A notable feature of LocalDate: when adding a month, the results correctly handle different month lengths. For example, January 31 + 1 month = February 28 (29 in a leap year).
For more complex operations, the Period class (for dates) and Duration class (for time) exist. The plus(TemporalAmount) and minus(TemporalAmount) methods accept these objects.
val today = LocalDate.now()
val nextWeek = today.plusDays(7)
val nextMonth = today.plusMonths(1)
val lastYear = today.minusYears(1)
val now = LocalTime.now()
val inTwoHours = now.plusHours(2)
val halfHourAgo = now.minusMinutes(30)
Let’s look at a practical example: a work shift tracking application. We need to calculate the shift duration and determine whether it falls during nighttime. We use LocalTime for start and end times, LocalDate for the date, and LocalDateTime for calculating shifts that cross midnight.
data class Shift(
val startTime: LocalTime,
val endTime: LocalTime,
val date: LocalDate
) {
fun isOvernight(): Boolean = endTime.isBefore(startTime)
fun durationInMinutes(): Long {
val start = LocalDateTime.of(date, startTime)
val end = LocalDateTime.of(
if (isOvernight()) date.plusDays(1) else date,
endTime
)
return Duration.between(start, end).toMinutes()
}
}
A second example — calculating a user’s age. We use LocalDate for the birth date and compare it with the current date, accounting for the day and month of birth.
fun calculateAge(birthDate: LocalDate): Int {
val today = LocalDate.now()
val period = Period.between(birthDate, today)
return period.years
}
A third example — working with notifications. LocalDateTime is used for scheduling reminders. We check whether the scheduled time has arrived.
data class Reminder(
val id: Long,
val scheduledAt: LocalDateTime
) {
fun isDue(): Boolean =
LocalDateTime.now().isAfter(scheduledAt)
}
Built-in support for java.time appeared on Android starting from API 26 (Android 8.0 Oreo). For devices with older Android versions, you need to use desugaring — a mechanism that adds support for new Java APIs in earlier versions.
Desugaring in the Android Gradle Plugin is configured through compileOptions in build.gradle. You simply need to set isCoreLibraryDesugaringEnabled = true and add the desugar_jdk_libs library. After that, java.time becomes available for all API levels starting from 14.
For projects that cannot use desugaring (for example, legacy projects on AGP below 4.0), there is the ThreeTenABP library — a java.time backport. It provides the same classes (LocalDate, LocalTime, LocalDateTime), but in the org.threeten.bp package.
@Suppress("UnstableApiUsage")
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
"coreLibraryDesugaring"("com.android.tools:desugar_jdk_libs:2.1.4")
}
The first common mistake — using LocalDateTime in distributed systems without considering timezones. If the server is in Europe/Moscow and the client is in Asia/Tokyo, LocalDateTime will be interpreted differently. Solution: use Instant or ZonedDateTime for global data.
The second mistake — incorrect string parsing. By default, LocalDate.parse() expects ISO-8601 format (yyyy-MM-dd). If the string is in a different format, you need to pass a DateTimeFormatter explicitly. You should also handle DateTimeParseException so that the application does not crash on invalid input.
The third mistake — ignoring null safety. LocalDate, LocalTime, and LocalDateTime are objects that can be null. In Kotlin, it is recommended to use nullable types with explicit checks or the Elvis operator. In Java — check for null before calling methods.
The fourth mistake — confusing LocalDateTime with ZonedDateTime. LocalDateTime does not contain any timezone information. If you need to pass an absolute moment in time — use zoned types. If local time is sufficient — use local types.
Frequently Asked Questions
Date stores the number of milliseconds from 1970-01-01 UTC, while LocalDate stores the year, month, and day without timezone binding. Date is mutable and not thread-safe, LocalDate is immutable and thread-safe. Date has been deprecated since Java 8.
Yes, LocalDateTime maps well to the SQL type TIMESTAMP WITHOUT TIME ZONE. JPA and Room support it via TypeConverter. For TIMESTAMP WITH TIME ZONE, use ZonedDateTime or OffsetDateTime.
Use ChronoUnit.DAYS.between(startDate, endDate). This method returns a long — the difference in days. For a more detailed calculation, use Period.between(), which returns a Period with years, months, and days.
LocalTime supports nanosecond precision (9 decimal places). If millisecond precision is sufficient, use truncateTo(ChronoUnit.MILLIS) before saving. This prevents rounding issues during serialization.
The now() method uses the device’s system clock and default timezone. If devices are in different timezones, the date may differ. For a unified timestamp, use Instant.now(), which always returns UTC time.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also