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 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.
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.
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 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 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.
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:
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.
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)
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).
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"))
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.
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")
)
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.
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()
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.
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)
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.
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.
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.
data class Reminder(
val id: Long,
val scheduledAt: LocalDateTime
) {
fun isDue(): Boolean =
LocalDateTime.now().isAfter(scheduledAt)
}
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.
@Suppress("UnstableApiUsage")
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
"coreLibraryDesugaring"("com.android.tools:desugar_jdk_libs:2.1.4")
}
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
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.
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.
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.
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.
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
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.
Leggi anche