Java LocalDateTime
Combinez date et heure sans information de fuseau horaire en Java avec LocalDateTime.
LocalDateTime est LocalDate et LocalTime assemblés — une date calendaire et une heure du jour, mais toujours sans fuseau horaire. C'est la valeur naturelle pour « cet événement s'est produit à 14h30 le 4 novembre », et c'est le type java.time le plus utilisé dans les bases de code réelles qui ne gèrent pas des utilisateurs à l'échelle mondiale.
L'API fluide a la même forme que les deux moitiés — mêmes fabriques now/of/parse, mêmes modificateurs plusX/minusX/withX, mêmes comparaisons isBefore/isAfter. Les parties intéressantes sont les nouvelles : la combinaison avec une date ou une heure, la décomposition inverse, et la conversion explicite vers ZonedDateTime quand le fuseau horaire devient enfin nécessaire.
Création
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 forme ISO-8601 comporte un T littéral entre la date et l'heure — c'est le séparateur standard. LocalDateTime.parse("2025-11-04 14:30:00") (espace au lieu du T) ne se parse pas avec le comportement par défaut ; il faudrait un DateTimeFormatter personnalisé pour cela.
Décomposition
LocalDate date = dt.toLocalDate();
LocalTime time = dt.toLocalTime();Ce sont les inverses de LocalDate.atTime(time) / LocalTime.atDate(date). Les deux sont de pures projections — aucune perte d'information, aucun fuseau horaire introduit.
Tous les accesseurs en une seule vue
LocalDateTime hérite du menu d'accesseurs des deux moitiés :
dt.getYear(); dt.getMonth(); dt.getMonthValue();
dt.getDayOfMonth(); dt.getDayOfWeek(); dt.getDayOfYear();
dt.getHour(); dt.getMinute(); dt.getSecond(); dt.getNano();Mêmes noms, mêmes sémantiques. Vous n'avez pas besoin d'appeler toLocalDate() ou toLocalTime() pour accéder aux éléments individuels — ils sont tous disponibles directement.
Arithmétique à travers la frontière
La différence cruciale avec LocalTime : LocalDateTime.plusHours(3) à 23:00 ne boucle pas silencieusement. Il passe au jour suivant :
LocalDateTime late = LocalDateTime.of(2025, 11, 4, 23, 0);
late.plusHours(3); // 2025-11-05T02:00 — date advanced as expectedC'est la raison même d'utiliser LocalDateTime plutôt que LocalTime pour tout calcul susceptible de franchir minuit. Le calcul est cohérent avec ce qu'on attendrait d'une horloge réelle qui connaît le jour en cours.
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 règle de troncature de plusMonths de LocalDate s'applique ici aussi : LocalDateTime.of(2025, 1, 31, 12, 0).plusMonths(1) donne 2025-02-28T12:00, et non 2025-03-03T12:00. La troncature porte uniquement sur la composante date ; l'heure reste inchangée.
Le fuseau horaire est absent intentionnellement
Un LocalDateTime n'est pas un instant sur la ligne de temps globale. LocalDateTime.of(2025, 11, 4, 9, 0) pourrait être 9h du matin à New York, 9h du matin à Berlin, ou 9h du matin à Tokyo — trois Instants bien distincts, et LocalDateTime ne vous dira pas lequel. Si deux LocalDateTimes sont égaux, cela signifie que les chaînes de date et d'heure sont identiques ; cela ne signifie pas que les instants sous-jacents sont identiques.
C'est une fonctionnalité, pas un bug. Pour « le contrat est signé à 14h00 heure locale où que se trouve le signataire », LocalDateTime est exactement la bonne forme. Pour « le serveur a reçu la requête à... », c'est la mauvaise forme — utilisez Instant. Pour « la réunion commence à 14h00 heure de New York », c'est encore la mauvaise forme — utilisez ZonedDateTime.
Pour convertir vers un instant zoné, vous devez ajouter le fuseau horaire explicitement :
ZonedDateTime ny = ldt.atZone(ZoneId.of("America/New_York"));
Instant inst = ldt.atZone(ZoneId.systemDefault()).toInstant();L'appel atZone(...) est l'appel déterminant — c'est le moment où le système de types vous oblige à choisir le fuseau horaire voulu. Une fois la décision prise, la conversion vers Instant est mécanique. Les deux chapitres suivants (ZonedDateTime, Instant) couvrent en détail les formes zonée et globale.
Comparaison
dt.isBefore(other);
dt.isAfter(other);
dt.isEqual(other);
dt.compareTo(other);L'ordre est lexicographique par (date, heure). Même avertissement qu'auparavant : deux LocalDateTimes se comparent selon leurs représentations textuelles de date et d'heure, et non selon les instants sous-jacents — parce qu'il n'y a pas d'instants sous-jacents sans fuseau horaire.
Distance
ChronoUnit.X.between fonctionne directement :
long minutes = ChronoUnit.MINUTES.between(start, end);
long days = ChronoUnit.DAYS.between(start, end);
Duration d = Duration.between(start, end);Duration.between fonctionne avec LocalDateTime (il fonctionne avec tout Temporal). Pour une arithmétique purement calendaire — « combien de mois entre ces deux LocalDateTimes » — utilisez ChronoUnit.MONTHS.between, qui renvoie un long, ou Period.between(start.toLocalDate(), end.toLocalDate()) pour la décomposition sous forme calendaire.
Un exemple concret : planification à travers minuit
Le programme ci-dessous utilise LocalDateTime pour un petit bout de code de planification : un poste de nuit qui commence à 22h00 et se termine à 06h00, en calculant correctement la durée à travers minuit ; en arrondissant « maintenant » au prochain quart d'heure ; en trouvant la prochaine occurrence d'une réunion récurrente à 09h30 ; et en illustrant la règle selon laquelle le fuseau horaire doit être ajouté explicitement lors de la conversion vers un instant dans le temps.
Ce qu'il faut retenir de l'exécution :
Duration.between(startShift, endShift)a produitPT8H. Le poste a franchi minuit, et les composantes de date ont absorbé le report — sans aucune ambiguïté. Le même calcul sur de simplesLocalTimes aurait renvoyéPT-16H(l'écueil de LocalTime du chapitre précédent). Pour une arithmétique susceptible de franchir minuit,LocalDateTimeest le type approprié.- En partant de
20:00,plusHours(3)est resté sur11-04(23:00, pas de franchissement de minuit) ;plusHours(5)a avancé jusqu'à11-05T01:00. La familleplus/minussurLocalDateTimepropage correctement les reports à travers toute la chaîneA/M/J/h/m/s/ns. Aucun cas particulier requis dans votre code. - Le bloc « prochaine réunion à 09h30 » a construit le 09:30 du jour avec trois appels
withX, puis a choisi entreaujourd'huietdemainen fonction deisBefore. C'est la forme habituelle pour « le prochain événement récurrent à cette heure » — assez court pour être inlinéé, assez fréquent pour être factorisé dans un helper si vous en avez plusieurs. - Le bloc « même LocalDateTime, fuseaux horaires différents » a produit deux
Instants différents distants de six heures. C'est la raison centrale pour laquelleLocalDateTimene prétend pas être un instant. La classe refuse de faire semblant qu'une date et une heure seules sont suffisantes ; c'est une étiquette sur une horloge murale quelque part, et laquelle dépend du fuseau horaire que vous fournissez. - Le contrôle d'immuabilité final a montré que
nowétait inchangé aprèsplusDays(7).withHour(0).withMinute(0). Cette garantie s'applique à toutes les opérations, à toutes les chaînes, à tous les helpers — il n'existe aucun moyen de muter unLocalDateTime. Passez-le librement, partagez-le entre threads, stockez-le dans uneMap.
La suite
LocalDateTime est le dernier des trois types « Local » — pas de fuseau horaire, aucune prétention d'être un instant. Le chapitre suivant, Java ZonedDateTime, ajoute le fuseau horaire explicitement : un LocalDateTime plus un ZoneId plus le décalage résolu pour cette heure locale dans ce fuseau, qui ensemble fixent un instant réel sur la ligne de temps globale.