Java LocalDateTime
Combina data e ora senza informazioni sul fuso orario in Java con LocalDateTime.
LocalDateTime è LocalDate e LocalTime uniti insieme — una data del calendario e un orario del giorno, ancora senza fuso orario. È il valore naturale per "questo evento è avvenuto alle 14:30 del 4 novembre," ed è il tipo java.time più usato nelle codebase reali che non gestiscono utenti globali.
L'API fluente ha la stessa forma delle due metà — le stesse factory now/of/parse, gli stessi modificatori plusX/minusX/withX, gli stessi confronti isBefore/isAfter. Le parti interessanti sono quelle nuove: la combinazione con una data o un orario, la decomposizione inversa, e la conversione esplicita a ZonedDateTime quando il fuso orario diventa rilevante.
Creazione
LocalDateTime now = LocalDateTime.now(); // JVM default zone
LocalDateTime made = LocalDateTime.of(2025, 11, 4, 14, 30);
LocalDateTime made2 = LocalDateTime.of(2025, Month.NOVEMBER, 4, 14, 30, 15);
LocalDateTime made3 = LocalDateTime.of(LocalDate.of(2025, 11, 4), LocalTime.of(14, 30));
LocalDateTime parsed = LocalDateTime.parse("2025-11-04T14:30:00"); // ISO-8601La forma ISO-8601 ha una T letterale tra la data e l'orario — è il separatore standard. LocalDateTime.parse("2025-11-04 14:30:00") (spazio invece di T) non viene interpretato con il parser predefinito; occorrerebbe un DateTimeFormatter personalizzato.
Decomposizione
LocalDate date = dt.toLocalDate();
LocalTime time = dt.toLocalTime();Questi metodi sono l'inverso di LocalDate.atTime(time) / LocalTime.atDate(date). Entrambi sono pura proiezione — nessuna perdita di informazioni, nessun fuso orario introdotto.
Tutti gli accessor in una volta
LocalDateTime eredita il menu di accessor da entrambe le metà:
dt.getYear(); dt.getMonth(); dt.getMonthValue();
dt.getDayOfMonth(); dt.getDayOfWeek(); dt.getDayOfYear();
dt.getHour(); dt.getMinute(); dt.getSecond(); dt.getNano();Stessi nomi, stessa semantica. Non è necessario chiamare toLocalDate() o toLocalTime() per accedere ai singoli campi — sono tutti disponibili direttamente.
Aritmetica oltre il confine
La differenza cruciale rispetto a LocalTime: LocalDateTime.plusHours(3) alle 23:00 non fa un wrap silenzioso. Avanza al giorno successivo:
LocalDateTime late = LocalDateTime.of(2025, 11, 4, 23, 0);
late.plusHours(3); // 2025-11-05T02:00 — date advanced as expectedQuesto è il motivo principale per usare LocalDateTime invece di LocalTime per qualsiasi calcolo che potrebbe attraversare la mezzanotte. La matematica è coerente con ciò che ci si aspetta da un orologio reale che conosce il giorno corrente.
dt.plusDays(7); dt.plusHours(36); dt.plusMinutes(150);
dt.minusYears(1); dt.minusSeconds(45);
dt.withYear(2026); dt.withHour(0); dt.withMinute(0);La regola di clamping di plusMonths da LocalDate si applica anche qui: LocalDateTime.of(2025, 1, 31, 12, 0).plusMonths(1) è 2025-02-28T12:00, non 2025-03-03T12:00. Il clamp riguarda solo la componente data; l'orario rimane invariato.
Il fuso orario è assente intenzionalmente
Un LocalDateTime non è un momento sulla linea temporale globale. LocalDateTime.of(2025, 11, 4, 9, 0) potrebbe essere le 9 del mattino a New York, le 9 a Berlino, o le 9 a Tokyo — tre Instant molto diversi, e LocalDateTime non indica quale. Se due LocalDateTime sono uguali, significa che le stringhe di data e ora sono uguali; ciò non significa che i momenti sottostanti siano uguali.
Questa è una caratteristica, non un bug. Per "il contratto è firmato alle 14:00 ora locale ovunque si trovi il firmatario," LocalDateTime è esattamente la forma giusta. Per "il server ha ricevuto la richiesta alle...", è la forma sbagliata — usa Instant. Per "la riunione inizia alle 14:00 ora di New York," sbagliata di nuovo — usa ZonedDateTime.
Per convertire a un momento con fuso orario, occorre aggiungere il fuso esplicitamente:
ZonedDateTime ny = ldt.atZone(ZoneId.of("America/New_York"));
Instant inst = ldt.atZone(ZoneId.systemDefault()).toInstant();atZone(...) è la chiamata fondamentale — è il momento in cui il sistema dei tipi ti costringe a decidere quale fuso intendi. Una volta deciso, la conversione a Instant è meccanica. I prossimi due capitoli (ZonedDateTime, Instant) trattano in dettaglio le forme con zona e globale.
Confronto
dt.isBefore(other);
dt.isAfter(other);
dt.isEqual(other);
dt.compareTo(other);L'ordinamento è lessicografico per (date, time). Lo stesso avvertimento di prima: due LocalDateTime vengono confrontati per le loro rappresentazioni stringa di data e ora, non per i momenti sottostanti — perché senza un fuso orario non esistono momenti sottostanti.
Distanza
ChronoUnit.X.between funziona direttamente:
long minutes = ChronoUnit.MINUTES.between(start, end);
long days = ChronoUnit.DAYS.between(start, end);
Duration d = Duration.between(start, end);Duration.between funziona su LocalDateTime (funziona su qualsiasi Temporal). Per l'aritmetica puramente calendaria — "quanti mesi tra questi due LocalDateTime" — usa ChronoUnit.MONTHS.between, che restituisce un long, oppure Period.between(start.toLocalDate(), end.toLocalDate()) per la scomposizione in formato calendario.
Un esempio pratico: pianificazione oltre la mezzanotte
Il programma seguente usa LocalDateTime per un piccolo pezzo di codice di pianificazione: un turno notturno che inizia alle 22:00 e termina alle 06:00, calcolando correttamente la durata attraverso la mezzanotte; arrotondando "adesso" al quarto d'ora successivo; trovando la prossima occorrenza di una riunione ricorrente alle 09:30; e dimostrando la regola che il fuso orario deve essere aggiunto esplicitamente quando si converte a un momento temporale.
Cosa ricavare dall'esecuzione:
Duration.between(startShift, endShift)ha prodottoPT8H. Il turno ha attraversato la mezzanotte e le componenti della data hanno gestito il riporto — nessuna ambiguità. Lo stesso calcolo su sempliciLocalTimeavrebbe restituitoPT-16H(il problema di LocalTime descritto nel capitolo precedente). Per l'aritmetica che potrebbe attraversare la mezzanotte,LocalDateTimeè il tipo corretto.- Partendo da
20:00,plusHours(3)è rimasto sull'11-04(23:00, nessun attraversamento della mezzanotte);plusHours(5)ha avanzato a11-05T01:00. La famigliaplus/minussuLocalDateTimepropaga i riporti correttamente attraverso l'intera catenaA/M/G/h/m/s/ns. Nessuna gestione speciale richiesta nel codice. - Il blocco "prossima riunione alle 09:30" ha costruito le 09:30 di oggi con tre chiamate
withX, poi ha scelto traoggiedomaniin base aisBefore. Questa è la forma comune per "il prossimo evento ricorrente a quest'orario del giorno" — abbastanza piccola da mettere inline, abbastanza comune da estrarre in un helper se ne hai molti. - Il blocco "stesso LocalDateTime, fusi diversi" ha prodotto due
Instantdiversi con sei ore di differenza. Questa è la ragione centrale per cuiLocalDateTimenon pretende di essere un momento. La classe rifiuta di far credere che una data e un orario da soli siano informazioni sufficienti; è un'etichetta su un orologio da muro da qualche parte, e quale orologio dipende dal fuso che fornisci. - Il controllo finale dell'immutabilità ha mostrato
nowinvariato dopoplusDays(7).withHour(0).withMinute(0). Questa garanzia vale su ogni operazione, ogni catena, ogni helper — non c'è modo di mutare unLocalDateTime. Passalo liberamente, condividilo tra thread, memorizzalo in unaMap.
Cosa viene dopo
LocalDateTime è l'ultimo dei tre tipi "Local" — nessun fuso, nessuna pretesa di essere un momento. Il prossimo capitolo, Java ZonedDateTime, aggiunge il fuso esplicitamente: un LocalDateTime più un ZoneId più l'offset risolto per quell'ora locale in quel fuso, che insieme identificano un momento effettivo sulla linea temporale globale.