LocalDate, LocalTime e LocalDateTime: cosa sono, lavoro con la data

Autore: IT Sectr Pubblicato: 2026-07-13 Tempo di lettura: 12 min

LocalDate, LocalTime e LocalDateTime sono le classi principali del pacchetto java.time che forniscono la gestione di data e ora senza vincoli di fuso orario. Secondo la documentazione Oracle (Java 17, 2024), questi tipi sono progettati come immutabili e thread-safe, rendendoli sicuri per applicazioni multithread. Sono diventati disponibili su Android tramite desugaring a partire dall'API 26 e per le versioni precedenti — tramite la libreria ThreeTenABP.

Punti chiave

  • LocalDate — una classe immutabile per rappresentare una data (anno, mese, giorno) senza ora e fuso orario.
  • LocalTime — una classe immutabile per rappresentare l'ora (ora, minuto, secondo, nanosecondo) senza data e fuso orario.
  • LocalDateTime — una combinazione di LocalDate e LocalTime che memorizza sia data che ora senza vincoli di fuso orario.
  • Tutte e tre le classi supportano operazioni aritmetiche — aggiunta e sottrazione di giorni, mesi, ore tramite i metodi plus e minus.
  • Su Android questi tipi sono disponibili tramite desugaring (API 26+) o la libreria ThreeTenABP (API < 26).

Cosa sono LocalDate, LocalTime e LocalDateTime?

LocalDate — una classe che rappresenta una data nel formato anno-mese-giorno senza informazioni di ora e fuso orario. Viene utilizzata per memorizzare dati come compleanni, date di eventi o date di scadenza.

LocalDate memorizza un anno nell'intervallo da -999999999 a +999999999, un mese da 1 a 12 e un giorno del mese considerando gli anni bisestili. La classe è completamente immutabile — qualsiasi operazione restituisce un nuovo oggetto.

LocalTime rappresenta l'ora del giorno: ore, minuti, secondi e nanosecondi. La precisione massima è fino a un nanosecondo. LocalTime non contiene informazioni di data o fuso orario, rendendolo comodo per memorizzare orari di apertura o durate di processi.

LocalDateTime combina LocalDate e LocalTime in un unico oggetto. È il tipo più utilizzato quando è necessario memorizzare sia data che ora, ma non è richiesto il vincolo del fuso orario. Ad esempio, la data e l'ora di un concerto in formato locale.

Secondo la documentazione Oracle Java (2024), tutte e tre le classi sono progettate sulla base di idee dalla libreria Joda-Time, ma con architettura migliorata e piena integrazione nella libreria standard.

Come funziona il pacchetto java.time?

Il pacchetto java.time è stato introdotto in Java 8 come sostituto delle obsolete classi Date, Calendar e SimpleDateFormat. La sua architettura è costruita sui principi di oggetti immutabili e interfaccia fluida.

Una caratteristica fondamentale — tutte le classi principali sono value-based. Ciò significa che le loro istanze vengono confrontate per valore, non per riferimento, e non possono essere ereditate. Per confrontare due oggetti, utilizzare il metodo equals, non l'operatore ==.

Il pacchetto è suddiviso in diverse categorie. Tipi senza fuso orario — LocalDate, LocalTime, LocalDateTime — vengono utilizzati per date e ore locali. Tipi con fuso orario — ZonedDateTime, OffsetDateTime, OffsetTime — aggiungono informazioni di offset o zona. Tipi istantanei — Instant — rappresentano un punto sulla linea temporale in UTC.

Questa separazione risolve un problema insito nella vecchia API: lo sviluppatore non sapeva mai se un oggetto Date contenesse informazioni sul fuso orario o meno. In java.time, ogni tipo dichiara esplicitamente la propria semantica.

LocalDate: lavoro con la data

La classe LocalDate fornisce molti metodi per creare, leggere e modificare le date. La data corrente può essere ottenuta tramite il metodo statico now(). Una data specifica — tramite il metodo of(int year, int month, int dayOfMonth).

Per leggere i componenti della data si utilizzano i getter: getYear(), getMonthValue(), getDayOfMonth(), getDayOfWeek(), getDayOfYear(). Il metodo getMonth() restituisce l'enumerazione Month, e getDayOfWeek() restituisce l'enumerazione DayOfWeek.

LocalDate supporta la verifica delle date. I metodi isBefore(), isAfter() e isEqual() consentono di confrontare le date. Il metodo isLeapYear() verifica se l'anno è bisestile. Il metodo lengthOfMonth() restituisce il numero di giorni nel mese, e lengthOfYear() il numero di giorni nell'anno.

Per la modifica, si utilizzano i metodi withYear(), withMonth(), withDayOfMonth(), che restituiscono un nuovo oggetto con il componente modificato. I metodi plusDays(), minusMonths() e simili eseguono l'aritmetica delle date.

LocalTime: lavoro con l'ora

LocalTime rappresenta l'ora del giorno con precisione al nanosecondo. Il formato standard è ISO-8601 (HH:mm:ss.nnnnnnnnn). Il valore minimo è 00:00, il massimo è 23:59:59.999999999.

Puoi creare un oggetto LocalTime usando now() per l'ora corrente, of(int hour, int minute), of(int hour, int minute, int second) o of(int hour, int minute, int second, int nanoOfSecond). Il metodo parse(CharSequence text) analizza una stringa nel formato ISO-8601.

I getter includono getHour(), getMinute(), getSecond(), getNano(). Il metodo toSecondOfDay() restituisce il numero di secondi dall'inizio del giorno, e toNanoOfDay() restituisce nanosecondi. Questo è utile per calcolare la durata all'interno di un singolo giorno.

LocalTime supporta le stesse operazioni di confronto e modifica di LocalDate: plusHours(), minusMinutes(), withHour(), withMinute(). I metodi isBefore() e isAfter() funzionano considerando che l'ora è ciclica all'interno di un giorno.

LocalDateTime: combinazione di data e ora

LocalDateTime combina le capacità di LocalDate e LocalTime in un'unica classe. Memorizza sia data che ora, ma senza fuso orario. È il tipo locale più flessibile, ma richiede cautela quando utilizzato in sistemi distribuiti.

Puoi creare LocalDateTime tramite i metodi statici now(), of(LocalDate date, LocalTime time), of(int year, Month month, int dayOfMonth, int hour, int minute) e i loro overload. Puoi anche combinare LocalDate e LocalTime tramite il metodo atTime().

LocalDateTime fornisce accesso a tutti i campi di data e ora tramite getter corrispondenti: toLocalDate() e toLocalTime() restituiscono componenti individuali. Il metodo truncatedTo(TemporalUnit unit) consente di arrotondare l'ora a una precisione specificata — ad esempio, ai minuti.

Per convertire in un fuso orario, utilizzare il metodo atZone(ZoneId zone), che restituisce ZonedDateTime. Questo è l'unico modo per aggiungere un fuso orario a LocalDateTime.

Come creare oggetti di data e ora?

Tutte e tre le classi utilizzano un modello di creazione unificato tramite metodi factory statici. I costruttori delle classi sono dichiarati private — non puoi creare un oggetto direttamente con new.

Metodi principali di creazione:

  • now() — data/ora corrente dall'orologio di sistema
  • of(...) — da componenti (anno, mese, giorno, ecc.)
  • parse(String) — da una stringa in formato ISO-8601
  • from(TemporalAccessor) — da un altro oggetto temporale

Il metodo of ha molti overload. Per LocalDate servono anno, mese e giorno. Per LocalTime — ore e minuti (opzionalmente secondi e nanosecondi). Per LocalDateTime — anno, mese, giorno, ore, minuti. Il mese può essere passato come int (1-12) o come enumerazione 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)

Conversione tra tipi

Le classi java.time sono progettate per una conversione conveniente tra loro. LocalDate può essere convertito in LocalDateTime tramite il metodo atTime(LocalTime) o atStartOfDay(). LocalTime — tramite atDate(LocalDate).

LocalDateTime può essere riconvertito in LocalDate tramite toLocalDate() e in LocalTime tramite toLocalTime(). Per convertire in ZonedDateTime, utilizzare il metodo atZone(ZoneId).

La conversione in java.util.Date (per compatibilità con codice legacy) richiede un passaggio intermedio tramite Instant e un fuso orario. Secondo Baeldung (2024), questa operazione viene eseguita tramite 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"))

Formattazione e parsing

Per la formattazione e il parsing, viene utilizzata la classe DateTimeFormatter. Fornisce formati predefiniti tramite costanti (ISO_LOCAL_DATE, ISO_LOCAL_TIME, ISO_LOCAL_DATE_TIME) e la possibilità di creare formati personalizzati tramite stringhe di pattern.

I pattern di formattazione utilizzano simboli: yyyy — anno, MM — mese (due cifre), dd — giorno, HH — ora (0-23), mm — minuto, ss — secondo. Il metodo format() viene chiamato sull'oggetto data-ora o tramite DateTimeFormatter.

DateTimeFormatter supporta anche la localizzazione tramite i metodi statici ofLocalizedDate(FormatStyle), ofLocalizedTime(FormatStyle) e ofLocalizedDateTime(FormatStyle). Gli stili disponibili sono SHORT, MEDIUM, LONG e 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")
)

Confronto di oggetti data-ora

Tutte e tre le classi implementano l'interfaccia Comparable, consentendo un confronto naturale. Il metodo compareTo() restituisce un numero negativo, zero o positivo a seconda dell'ordine. I metodi isBefore(), isAfter() e isEqual() restituiscono un booleano.

Per LocalDate, il confronto è cronologico — una data precedente è considerata minore. Per LocalTime — per ora del giorno. Per LocalDateTime — prima per data, poi per ora. Tutti i confronti considerano correttamente gli anni bisestili e il numero di giorni nei mesi.

Una differenza importante rispetto alla vecchia API: equals() per LocalDate, LocalTime e LocalDateTime confronta i valori, non i riferimenti. Ciò significa che due oggetti con gli stessi campi saranno uguali, anche se sono istanze diverse.

kotlin
val d1 = LocalDate.of(2026, 7, 21)
val d2 = LocalDate.of(2026, 12, 25)

if (d1.isBefore(d2)) {
    Log.d("Data", "d1 è prima di d2")
}

val sortedDates = listOf(d2, d1).sorted()

Aritmetica di data e ora

Tutte e tre le classi supportano operazioni aritmetiche tramite i metodi plus e minus. Per LocalDate sono disponibili plusDays(), plusWeeks(), plusMonths(), plusYears() e i corrispondenti metodi minus. LocalTime supporta plusHours(), plusMinutes(), plusSeconds(), plusNanos().

LocalDateTime eredita tutte le operazioni aritmetiche da entrambi i tipi. Una caratteristica notevole di LocalDate: quando si aggiunge un mese, i risultati gestiscono correttamente diverse lunghezze dei mesi. Ad esempio, 31 gennaio + 1 mese = 28 (29 in anno bisestile) febbraio.

Per operazioni più complesse, esistono le classi Period (per le date) e Duration (per il tempo). I metodi plus(TemporalAmount) e minus(TemporalAmount) accettano questi oggetti.

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)

Esempi di codice in Kotlin

Consideriamo un esempio pratico: un'applicazione per il tracciamento dei turni di lavoro. Dobbiamo calcolare la durata del turno e determinare se cade in orario notturno. Utilizziamo LocalTime per gli orari di inizio e fine, LocalDate per la data e LocalDateTime per calcolare i turni che attraversano la mezzanotte.

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 secondo esempio — calcolo dell'età di un utente. Utilizziamo LocalDate per la data di nascita e la confrontiamo con la data corrente, considerando il giorno e il mese di nascita.

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

Un terzo esempio — lavoro con le notifiche. LocalDateTime viene utilizzato per pianificare promemoria. Verifichiamo se l'ora pianificata è arrivata.

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

Supporto Android: livello API e desugaring

Il supporto integrato per java.time è apparso su Android a partire dall'API 26 (Android 8.0 Oreo). Per i dispositivi con versioni Android precedenti, è necessario utilizzare il desugaring — un meccanismo che aggiunge il supporto per le nuove API Java nelle versioni precedenti.

Il desugaring in Android Gradle Plugin viene configurato tramite compileOptions in build.gradle. Basta impostare isCoreLibraryDesugaringEnabled = true e aggiungere la libreria desugar_jdk_libs. Dopodiché, java.time diventa disponibile per tutti i livelli API a partire da 14.

Per i progetti che non possono utilizzare il desugaring (ad esempio, progetti legacy con AGP inferiore a 4.0), esiste la libreria ThreeTenABP — un backport di java.time. Fornisce le stesse classi (LocalDate, LocalTime, LocalDateTime), ma nel pacchetto org.threeten.bp.

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

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

Errori comuni e come evitarli

Il primo errore comune — utilizzare LocalDateTime in sistemi distribuiti senza considerare i fusi orari. Se il server è in Europe/Moscow e il client in Asia/Tokyo, LocalDateTime verrà interpretato diversamente. Soluzione: utilizzare Instant o ZonedDateTime per i dati globali.

Il secondo errore — parsing errato delle stringhe. Per impostazione predefinita, LocalDate.parse() si aspetta il formato ISO-8601 (yyyy-MM-dd). Se la stringa è in un formato diverso, è necessario passare esplicitamente un DateTimeFormatter. È necessario gestire anche DateTimeParseException per evitare che l'applicazione si blocchi con input non validi.

Il terzo errore — ignorare la sicurezza null. LocalDate, LocalTime e LocalDateTime sono oggetti che possono essere null. In Kotlin, si raccomanda di utilizzare tipi nullable con controlli espliciti o l'operatore Elvis. In Java — controllare null prima di chiamare i metodi.

Il quarto errore — confondere LocalDateTime con ZonedDateTime. LocalDateTime non contiene informazioni sul fuso orario. Se è necessario passare un momento assoluto nel tempo — utilizzare tipi zonali. Se l'ora locale è sufficiente — utilizzare tipi locali.

Domande frequenti

Qual è la differenza tra LocalDate e Date in Java?

Date memorizza il numero di millisecondi dal 1970-01-01 UTC, mentre LocalDate memorizza anno, mese e giorno senza vincoli di fuso orario. Date è mutabile e non thread-safe, LocalDate è immutabile e thread-safe. Date è obsoleto da Java 8.

Si può utilizzare LocalDateTime in un database?

Sì, LocalDateTime si mappa bene sul tipo SQL TIMESTAMP WITHOUT TIME ZONE. JPA e Room lo supportano tramite TypeConverter. Per TIMESTAMP WITH TIME ZONE, utilizzare ZonedDateTime o OffsetDateTime.

Come ottenere il numero di giorni tra due date?

Utilizzare ChronoUnit.DAYS.between(startDate, endDate). Questo metodo restituisce un long — la differenza in giorni. Per un calcolo più dettagliato, utilizzare Period.between(), che restituisce un Period con anni, mesi e giorni.

Cosa fare se è necessario preservare la precisione del tempo fino ai millisecondi?

LocalTime supporta la precisione al nanosecondo (9 decimali). Se la precisione al millisecondo è sufficiente, utilizzare truncateTo(ChronoUnit.MILLIS) prima di salvare. Questo previene problemi di arrotondamento durante la serializzazione.

Perché LocalDate.now() restituisce date diverse su dispositivi diversi?

Il metodo now() utilizza l'orologio di sistema e il fuso orario predefinito del dispositivo. Se i dispositivi si trovano in fusi orari diversi, la data può differire. Per un timestamp unificato, utilizzare Instant.now(), che restituisce sempre l'ora UTC.

Riepilogo

  • LocalDate — una classe immutabile per data senza ora e fuso orario. Utilizzata per memorizzare compleanni, scadenze, date di eventi.
  • LocalTime — una classe immutabile per ora del giorno con precisione al nanosecondo. Adatta per memorizzare orari di apertura, durate di processi.
  • LocalDateTime — combinazione di data e ora senza vincoli di fuso orario. Il tipo locale più flessibile, ma non adatto a sistemi distribuiti.
  • Tutte e tre le classi supportano aritmetica, confronto, formattazione e parsing tramite un'API unificata basata su DateTimeFormatter.
  • Su Android, java.time è disponibile tramite supporto integrato dall'API 26 o tramite desugaring per versioni precedenti.
  • Per timestamp globali e dati con fuso orario, utilizzare ZonedDateTime o Instant invece dei tipi locali.
  • Durante il parsing di stringhe, passare sempre un DateTimeFormatter per formati non standard e gestire DateTimeParseException.

Svilupperemo un'applicazione mobile chiavi in mano

IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.

Discuti il progetto

Leggi anche