LocalDate, LocalTime and LocalDateTime: What They Are, Working with Date

Author: IT Sectr Published: 2026-07-13 Reading time: 12 min

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 — an immutable class for representing a date (year, month, day) without time and timezone.
  • LocalTime — an immutable class for representing time (hour, minute, second, nanosecond) without date and timezone.
  • LocalDateTime — a combination of LocalDate and LocalTime that stores both date and time without timezone binding.
  • All three classes support arithmetic operations — adding and subtracting days, months, hours via plus and minus methods.
  • On Android these types are available through desugaring (API 26+) or the ThreeTenABP library (API < 26).

What Are LocalDate, LocalTime and LocalDateTime?

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.

How Does the java.time Package Work?

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.

LocalDate: Working with Date

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: Working with Time

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: Date and Time Combination

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.

How to Create Date and Time Objects?

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:

  • now() — current date/time from the system clock
  • of(...) — from components (year, month, day, etc.)
  • parse(String) — from a string in ISO-8601 format
  • from(TemporalAccessor) — from another temporal object

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.

kotlin
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)

Conversion Between Types

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).

kotlin
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"))

Formatting and Parsing

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.

kotlin
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")
)

Comparing Date-Time Objects

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.

kotlin
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()

Date and Time Arithmetic

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.

kotlin
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)

Kotlin Code Examples

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.

kotlin
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.

kotlin
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.

kotlin
data class Reminder(
    val id: Long,
    val scheduledAt: LocalDateTime
) {
    fun isDue(): Boolean =
        LocalDateTime.now().isAfter(scheduledAt)
}

Android Support: API Level and Desugaring

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.

groovy
@Suppress("UnstableApiUsage")
android {
    compileOptions {
        isCoreLibraryDesugaringEnabled = true
    }
}

dependencies {
    "coreLibraryDesugaring"("com.android.tools:desugar_jdk_libs:2.1.4")
}

Common Mistakes and How to Avoid Them

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

What is the difference between LocalDate and Date in Java?

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.

Can LocalDateTime be used in a database?

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.

How to get the number of days between two dates?

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.

What to do if you need to preserve time precision down to milliseconds?

LocalTime supports nanosecond precision (9 decimal places). If millisecond precision is sufficient, use truncateTo(ChronoUnit.MILLIS) before saving. This prevents rounding issues during serialization.

Why does LocalDate.now() return different dates on different devices?

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

  • LocalDate — an immutable class for date without time and timezone. Used for storing birthdays, deadlines, event dates.
  • LocalTime — an immutable class for time of day with nanosecond precision. Suitable for storing opening hours, process durations.
  • LocalDateTime — a combination of date and time without timezone binding. The most flexible local type, but not suitable for distributed systems.
  • All three classes support arithmetic, comparison, formatting, and parsing through a unified API based on DateTimeFormatter.
  • On Android, java.time is available through built-in support from API 26 or via desugaring for older versions.
  • For global timestamps and timezone-aware data, use ZonedDateTime or Instant instead of local types.
  • When parsing strings, always pass a DateTimeFormatter for non-standard formats and handle DateTimeParseException.

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.

Discuss the project

Read also