LocalDate, LocalTime y LocalDateTime: qué son, trabajo con fecha

Autor: IT Sectr Publicado: 2026-07-13 Tiempo de lectura: 12 min

LocalDate, LocalTime y LocalDateTime son las clases principales del paquete java.time que proporcionan manejo de fecha y hora sin vinculación a zona horaria. Según la documentación de Oracle (Java 17, 2024), estos tipos están diseñados como inmutables y thread-safe, lo que los hace seguros para aplicaciones multiproceso. Están disponibles en Android mediante desugaring a partir de API 26, y para versiones anteriores — a través de la biblioteca ThreeTenABP.

Puntos clave

  • LocalDate — una clase inmutable para representar una fecha (año, mes, día) sin hora ni zona horaria.
  • LocalTime — una clase inmutable para representar la hora (hora, minuto, segundo, nanosegundo) sin fecha ni zona horaria.
  • LocalDateTime — una combinación de LocalDate y LocalTime que almacena fecha y hora sin vinculación a zona horaria.
  • Las tres clases admiten operaciones aritméticas — suma y resta de días, meses, horas mediante los métodos plus y minus.
  • En Android estos tipos están disponibles mediante desugaring (API 26+) o la biblioteca ThreeTenABP (API < 26).

¿Qué son LocalDate, LocalTime y LocalDateTime?

LocalDate — una clase que representa una fecha en formato año-mes-día sin información de hora ni zona horaria. Se utiliza para almacenar datos como cumpleaños, fechas de eventos o fechas de vencimiento.

LocalDate almacena un año en el rango de -999999999 a +999999999, un mes de 1 a 12 y un día del mes considerando años bisiestos. La clase es completamente inmutable — cualquier operación devuelve un nuevo objeto.

LocalTime representa la hora del día: horas, minutos, segundos y nanosegundos. La precisión máxima es de hasta un nanosegundo. LocalTime no contiene información de fecha ni zona horaria, lo que lo hace conveniente para almacenar horarios de apertura o duración de procesos.

LocalDateTime combina LocalDate y LocalTime en un solo objeto. Es el tipo más utilizado cuando se necesita almacenar fecha y hora, pero no se requiere vinculación a zona horaria. Por ejemplo, la fecha y hora de un concierto en formato local.

Según Oracle Java Documentation (2024), las tres clases están diseñadas basándose en ideas de la biblioteca Joda-Time, pero con una arquitectura mejorada y total integración en la biblioteca estándar.

¿Cómo funciona el paquete java.time?

El paquete java.time se introdujo en Java 8 como reemplazo de las obsoletas clases Date, Calendar y SimpleDateFormat. Su arquitectura se basa en los principios de objetos inmutables e interfaz fluida.

Una característica clave es que todas las clases principales son value-based. Esto significa que sus instancias se comparan por valor, no por referencia, y no se pueden heredar. Para comparar dos objetos se usa el método equals, no el operador ==.

El paquete se divide en varias categorías. Tipos sin zona horaria — LocalDate, LocalTime, LocalDateTime — se usan para fechas y horas locales. Tipos con zona horaria — ZonedDateTime, OffsetDateTime, OffsetTime — añaden información de desplazamiento o zona. Tipos instantáneos — Instant — representan un punto en la línea de tiempo en UTC.

Esta separación resuelve un problema inherente a la API anterior: el desarrollador nunca sabía si un objeto Date contenía información de zona horaria o no. En java.time, cada tipo declara explícitamente su semántica.

LocalDate: trabajo con fecha

La clase LocalDate proporciona muchos métodos para crear, leer y modificar fechas. La fecha actual se obtiene mediante el método estático now(). Una fecha específica — mediante el método of(int year, int month, int dayOfMonth).

Para leer los componentes de la fecha se utilizan getters: getYear(), getMonthValue(), getDayOfMonth(), getDayOfWeek(), getDayOfYear(). El método getMonth() devuelve el enum Month, y getDayOfWeek() devuelve el enum DayOfWeek.

LocalDate admite comprobaciones de fecha. Los métodos isBefore(), isAfter() e isEqual() permiten comparar fechas. El método isLeapYear() comprueba si el año es bisiesto. El método lengthOfMonth() devuelve el número de días del mes, y lengthOfYear() el número de días del año.

Para modificar se usan los métodos withYear(), withMonth(), withDayOfMonth(), que devuelven un nuevo objeto con el componente modificado. Los métodos plusDays(), minusMonths() y similares realizan aritmética de fechas.

LocalTime: trabajo con hora

LocalTime representa la hora del día con precisión de nanosegundos. El formato estándar es ISO-8601 (HH:mm:ss.nnnnnnnnn). El valor mínimo es 00:00, el máximo es 23:59:59.999999999.

Se puede crear un objeto LocalTime usando now() para la hora actual, of(int hour, int minute), of(int hour, int minute, int second) o of(int hour, int minute, int second, int nanoOfSecond). El método parse(CharSequence text) analiza una cadena en formato ISO-8601.

Los getters incluyen getHour(), getMinute(), getSecond(), getNano(). El método toSecondOfDay() devuelve el número de segundos desde el inicio del día, y toNanoOfDay() devuelve nanosegundos. Esto es útil para calcular la duración dentro de un mismo día.

LocalTime admite las mismas operaciones de comparación y modificación que LocalDate: plusHours(), minusMinutes(), withHour(), withMinute(). Los métodos isBefore() e isAfter() funcionan considerando que la hora es cíclica dentro de un día.

LocalDateTime: combinación de fecha y hora

LocalDateTime combina las capacidades de LocalDate y LocalTime en una sola clase. Almacena fecha y hora, pero sin zona horaria. Es el tipo local más flexible, pero requiere precaución al usarse en sistemas distribuidos.

Se puede crear LocalDateTime mediante los métodos estáticos now(), of(LocalDate date, LocalTime time), of(int year, Month month, int dayOfMonth, int hour, int minute) y sus sobrecargas. También se puede combinar LocalDate y LocalTime mediante el método atTime().

LocalDateTime proporciona acceso a todos los campos de fecha y hora a través de getters correspondientes: toLocalDate() y toLocalTime() devuelven componentes individuales. El método truncatedTo(TemporalUnit unit) permite redondear la hora a una precisión determinada — por ejemplo, a minutos.

Para convertir a una zona horaria, se usa el método atZone(ZoneId zone), que devuelve ZonedDateTime. Esta es la única forma de añadir una zona horaria a LocalDateTime.

¿Cómo crear objetos de fecha y hora?

Las tres clases utilizan un patrón de creación unificado mediante métodos estáticos de fábrica. Los constructores de las clases están declarados como private — no se puede crear un objeto directamente con new.

Métodos principales de creación:

  • now() — fecha/hora actual del reloj del sistema
  • of(...) — a partir de componentes (año, mes, día, etc.)
  • parse(String) — a partir de una cadena en formato ISO-8601
  • from(TemporalAccessor) — a partir de otro objeto temporal

El método of tiene muchas sobrecargas. Para LocalDate se necesitan año, mes y día. Para LocalTime — horas y minutos (opcionalmente segundos y nanosegundos). Para LocalDateTime — año, mes, día, horas, minutos. El mes se puede pasar como int (1-12) o como el enum Month.

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)

Conversión entre tipos

Las clases de java.time están diseñadas para una conversión conveniente entre sí. LocalDate se puede convertir a LocalDateTime mediante el método atTime(LocalTime) o atStartOfDay(). LocalTime — mediante atDate(LocalDate).

LocalDateTime se puede convertir de vuelta a LocalDate mediante toLocalDate() y a LocalTime mediante toLocalTime(). Para convertir a ZonedDateTime, se usa el método atZone(ZoneId).

La conversión a java.util.Date (para compatibilidad con código heredado) requiere un paso intermedio a través de Instant y una zona horaria. Según Baeldung (2024), esta operación se realiza mediante 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"))

Formateo y análisis

Para formatear y analizar, se utiliza la clase DateTimeFormatter. Proporciona formatos predefinidos mediante constantes (ISO_LOCAL_DATE, ISO_LOCAL_TIME, ISO_LOCAL_DATE_TIME) y la posibilidad de crear formatos personalizados mediante cadenas de patrón.

Los patrones de formato usan símbolos: yyyy — año, MM — mes (dos dígitos), dd — día, HH — hora (0-23), mm — minuto, ss — segundo. El método format() se llama en el objeto fecha-hora o mediante DateTimeFormatter.

DateTimeFormatter también admite localización mediante los métodos estáticos ofLocalizedDate(FormatStyle), ofLocalizedTime(FormatStyle) y ofLocalizedDateTime(FormatStyle). Los estilos disponibles son SHORT, MEDIUM, LONG y 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")
)

Comparación de objetos fecha-hora

Las tres clases implementan la interfaz Comparable, lo que permite compararlas de forma natural. El método compareTo() devuelve un número negativo, cero o positivo según el orden. Los métodos isBefore(), isAfter() e isEqual() devuelven un booleano.

Para LocalDate, la comparación es cronológica — una fecha anterior se considera menor. Para LocalTime — por hora del día. Para LocalDateTime — primero por fecha, luego por hora. Todas las comparaciones consideran correctamente los años bisiestos y el número de días de los meses.

Una diferencia importante con la API anterior: equals() para LocalDate, LocalTime y LocalDateTime compara valores, no referencias. Esto significa que dos objetos con los mismos campos serán iguales, incluso si son instancias diferentes.

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

Aritmética de fecha y hora

Las tres clases admiten operaciones aritméticas mediante los métodos plus y minus. Para LocalDate están disponibles plusDays(), plusWeeks(), plusMonths(), plusYears() y los métodos minus correspondientes. LocalTime admite plusHours(), plusMinutes(), plusSeconds(), plusNanos().

LocalDateTime hereda todas las operaciones aritméticas de ambos tipos. Una característica notable de LocalDate: al agregar un mes, los resultados manejan correctamente las diferentes longitudes de los meses. Por ejemplo, 31 de enero + 1 mes = 28 (29 en año bisiesto) de febrero.

Para operaciones más complejas existen las clases Period (para fechas) y Duration (para tiempo). Los métodos plus(TemporalAmount) y minus(TemporalAmount) aceptan estos objetos.

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)

Ejemplos de código en Kotlin

Veamos un ejemplo práctico: una aplicación de registro de turnos de trabajo. Necesitamos calcular la duración del turno y determinar si cae en horario nocturno. Usamos LocalTime para las horas de inicio y fin, LocalDate para la fecha y LocalDateTime para calcular turnos que cruzan la medianoche.

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

Un segundo ejemplo — cálculo de la edad de un usuario. Usamos LocalDate para la fecha de nacimiento y la comparamos con la fecha actual, considerando el día y mes de nacimiento.

kotlin
fun calculateAge(birthDate: LocalDate): Int {
    val today = LocalDate.now()
    val period = Period.between(birthDate, today)
    return period.years
}

Un tercer ejemplo — trabajo con notificaciones. LocalDateTime se usa para programar recordatorios. Verificamos si la hora programada ha llegado.

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

Compatibilidad con Android: nivel de API y desugaring

El soporte integrado de java.time apareció en Android a partir de API 26 (Android 8.0 Oreo). Para dispositivos con versiones anteriores de Android, es necesario usar desugaring — un mecanismo que añade soporte para nuevas API de Java en versiones anteriores.

El desugaring en Android Gradle Plugin se configura mediante compileOptions en build.gradle. Solo necesita establecer isCoreLibraryDesugaringEnabled = true y agregar la biblioteca desugar_jdk_libs. Después de eso, java.time está disponible para todos los niveles de API a partir de 14.

Para proyectos que no pueden usar desugaring (por ejemplo, proyectos heredados con AGP inferior a 4.0), existe la biblioteca ThreeTenABP — un backport de java.time. Proporciona las mismas clases (LocalDate, LocalTime, LocalDateTime), pero en el paquete org.threeten.bp.

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

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

Errores comunes y cómo evitarlos

El primer error común — usar LocalDateTime en sistemas distribuidos sin considerar las zonas horarias. Si el servidor está en Europe/Moscow y el cliente en Asia/Tokyo, LocalDateTime se interpretará de forma diferente. Solución: use Instant o ZonedDateTime para datos globales.

El segundo error — análisis incorrecto de cadenas. Por defecto, LocalDate.parse() espera el formato ISO-8601 (yyyy-MM-dd). Si la cadena está en otro formato, debe pasar un DateTimeFormatter explícitamente. También debe manejar DateTimeParseException para que la aplicación no se bloquee con entrada no válida.

El tercer error — ignorar la seguridad de null. LocalDate, LocalTime y LocalDateTime son objetos que pueden ser null. En Kotlin, se recomienda usar tipos nullable con comprobaciones explícitas o el operador Elvis. En Java — comprobar null antes de llamar a métodos.

El cuarto error — confundir LocalDateTime con ZonedDateTime. LocalDateTime no contiene información de zona horaria. Si necesita pasar un momento absoluto en el tiempo — use tipos zonales. Si la hora local es suficiente — use tipos locales.

Preguntas frecuentes

¿Cuál es la diferencia entre LocalDate y Date en Java?

Date almacena el número de milisegundos desde 1970-01-01 UTC, mientras que LocalDate almacena el año, mes y día sin vinculación a zona horaria. Date es mutable y no thread-safe, LocalDate es inmutable y thread-safe. Date está obsoleto desde Java 8.

¿Se puede usar LocalDateTime en una base de datos?

Sí, LocalDateTime se asigna bien al tipo SQL TIMESTAMP WITHOUT TIME ZONE. JPA y Room lo admiten mediante TypeConverter. Para TIMESTAMP WITH TIME ZONE, use ZonedDateTime u OffsetDateTime.

¿Cómo obtener el número de días entre dos fechas?

Use ChronoUnit.DAYS.between(startDate, endDate). Este método devuelve un long — la diferencia en días. Para un cálculo más detallado, use Period.between(), que devuelve un Period con años, meses y días.

¿Qué hacer si necesita preservar la precisión del tiempo hasta milisegundos?

LocalTime admite precisión de nanosegundos (9 decimales). Si la precisión de milisegundos es suficiente, use truncateTo(ChronoUnit.MILLIS) antes de guardar. Esto evita problemas de redondeo durante la serialización.

¿Por qué LocalDate.now() devuelve fechas diferentes en distintos dispositivos?

El método now() usa el reloj del sistema y la zona horaria predeterminada del dispositivo. Si los dispositivos están en diferentes zonas horarias, la fecha puede diferir. Para una marca de tiempo unificada, use Instant.now(), que siempre devuelve la hora en UTC.

Resumen

  • LocalDate — una clase inmutable para fecha sin hora ni zona horaria. Se usa para almacenar cumpleaños, fechas límite, fechas de eventos.
  • LocalTime — una clase inmutable para hora del día con precisión de nanosegundos. Adecuado para almacenar horarios de apertura, duraciones de procesos.
  • LocalDateTime — combinación de fecha y hora sin vinculación a zona horaria. El tipo local más flexible, pero no adecuado para sistemas distribuidos.
  • Las tres clases admiten aritmética, comparación, formateo y análisis a través de una API unificada basada en DateTimeFormatter.
  • En Android, java.time está disponible mediante soporte integrado desde API 26 o mediante desugaring para versiones anteriores.
  • Para marcas de tiempo globales y datos con zona horaria, use ZonedDateTime o Instant en lugar de tipos locales.
  • Al analizar cadenas, siempre pase un DateTimeFormatter para formatos no estándar y maneje DateTimeParseException.

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también