Java LocalDateTime
Combine data e hora sem fuso horário em Java com LocalDateTime. Veja como criar, calcular e converter.
LocalDateTime é LocalDate e LocalTime unidos — uma data do calendário e um horário do dia, mas ainda sem fuso horário. É o valor natural para "este evento ocorreu às 14:30 do dia 4 de novembro", e é o tipo java.time mais utilizado em codebases reais que não lidam com usuários globais.
A API fluente tem a mesma forma que as duas metades — as mesmas factories now/of/parse, os mesmos modificadores plusX/minusX/withX, as mesmas comparações isBefore/isAfter. As partes interessantes são as novas: combinar com uma data ou hora, decompor novamente e a conversão explícita para ZonedDateTime quando o fuso finalmente importa.
Criando
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-8601O formato ISO-8601 tem um T literal entre a data e a hora — esse é o separador padrão. LocalDateTime.parse("2025-11-04 14:30:00") (espaço em vez de T) não é analisado com o padrão; você precisaria de um DateTimeFormatter personalizado para isso.
Decomposição
LocalDate date = dt.toLocalDate();
LocalTime time = dt.toLocalTime();São o inverso de LocalDate.atTime(time) / LocalTime.atDate(date). Ambos são projeção pura — sem perda de informação, sem introdução de fuso.
Todos os acessores de uma vez
LocalDateTime herda o menu de acessores de ambas as metades:
dt.getYear(); dt.getMonth(); dt.getMonthValue();
dt.getDayOfMonth(); dt.getDayOfWeek(); dt.getDayOfYear();
dt.getHour(); dt.getMinute(); dt.getSecond(); dt.getNano();Mesmos nomes, mesma semântica. Não é necessário chamar toLocalDate() ou toLocalTime() para acessar as partes individuais — todas estão disponíveis diretamente.
Aritmética além do limite
A diferença crucial em relação ao LocalTime: LocalDateTime.plusHours(3) em 23:00 não volta silenciosamente ao início. Ele avança para o dia seguinte:
LocalDateTime late = LocalDateTime.of(2025, 11, 4, 23, 0);
late.plusHours(3); // 2025-11-05T02:00 — date advanced as expectedEsse é o principal motivo para usar LocalDateTime em vez de LocalTime em qualquer cálculo que possa cruzar a meia-noite. A matemática é consistente com o que se espera de um relógio real que sabe que dia é.
dt.plusDays(7); dt.plusHours(36); dt.plusMinutes(150);
dt.minusYears(1); dt.minusSeconds(45);
dt.withYear(2026); dt.withHour(0); dt.withMinute(0);A regra de truncamento de plusMonths do LocalDate também se aplica aqui: LocalDateTime.of(2025, 1, 31, 12, 0).plusMonths(1) é 2025-02-28T12:00, não 2025-03-03T12:00. O truncamento ocorre apenas no componente de data; a hora não é alterada.
O fuso está ausente por design
Um LocalDateTime não é um momento na linha do tempo global. LocalDateTime.of(2025, 11, 4, 9, 0) pode ser 9h em Nova York, 9h em Berlim ou 9h em Tóquio — três Instants muito diferentes, e LocalDateTime não dirá qual. Se dois LocalDateTimes são iguais, isso significa que as strings de data e hora são iguais; não significa que os momentos subjacentes são iguais.
Isso é uma característica, não um bug. Para "o contrato é assinado às 14:00 no horário local de onde o signatário estiver", LocalDateTime é exatamente a forma certa. Para "o servidor recebeu a requisição às...", é a forma errada — use Instant. Para "a reunião começa às 14:00 no horário de Nova York", também está errado — use ZonedDateTime.
Para converter para um momento com fuso, você precisa adicionar o fuso explicitamente:
ZonedDateTime ny = ldt.atZone(ZoneId.of("America/New_York"));
Instant inst = ldt.atZone(ZoneId.systemDefault()).toInstant();O atZone(...) é a chamada essencial — é o momento em que o sistema de tipos obriga você a decidir qual fuso você quer dizer. Após a decisão, a conversão para Instant é mecânica. Os dois próximos capítulos (ZonedDateTime, Instant) cobrem as formas com fuso e global em detalhes.
Comparando
dt.isBefore(other);
dt.isAfter(other);
dt.isEqual(other);
dt.compareTo(other);A ordenação é lexicográfica por (date, time). O mesmo aviso de antes: dois LocalDateTimes são comparados pelas suas representações em string de data e hora, não pelos momentos subjacentes — porque não há momentos subjacentes sem um fuso.
Distância
ChronoUnit.X.between funciona diretamente:
long minutes = ChronoUnit.MINUTES.between(start, end);
long days = ChronoUnit.DAYS.between(start, end);
Duration d = Duration.between(start, end);Duration.between funciona com LocalDateTime (funciona em qualquer Temporal). Para aritmética puramente calendária — "quantos meses entre estes dois LocalDateTimes" — use ChronoUnit.MONTHS.between, que retorna um long, ou Period.between(start.toLocalDate(), end.toLocalDate()) para a decomposição em formato calendário.
Um exemplo prático: agendamento através da meia-noite
O programa abaixo usa LocalDateTime para um pequeno trecho de código de agendamento: um turno noturno que começa às 22:00 e termina às 06:00, calculando a duração corretamente através da meia-noite; arredondando "agora" para o próximo quarto de hora; encontrando a próxima ocorrência de uma reunião recorrente às 09:30; e demonstrando a regra de que o fuso deve ser adicionado explicitamente ao converter para um momento no tempo.
O que extrair da execução:
Duration.between(startShift, endShift)produziuPT8H. O turno cruzou a meia-noite, e os componentes de data carregaram o transporte — não havia ambiguidade. O mesmo cálculo comLocalTimes puros teria retornadoPT-16H(o problema do LocalTime do capítulo anterior). Para aritmética que pode cruzar a meia-noite,LocalDateTimeé o tipo certo.- Começando de
20:00,plusHours(3)ficou em11-04(23:00, sem cruzar a meia-noite);plusHours(5)avançou para11-05T01:00. A famíliaplus/minusemLocalDateTimepropaga os transportes corretamente por toda a cadeiaY/M/D/h/m/s/ns. Não é necessário tratamento especial no seu código. - O bloco "próxima reunião às 09:30" construiu o horário de hoje às 09:30 com três chamadas
withX, depois escolheu entrehojeeamanhãcom base emisBefore. Essa é a forma comum para "o próximo evento recorrente neste horário do dia" — pequeno o suficiente para deixar inline, comum o suficiente para extrair para um helper se você tiver vários deles. - O bloco "mesmo LocalDateTime, fusos diferentes" produziu dois
Instants diferentes com seis horas de diferença. Esse é o motivo central pelo qualLocalDateTimenão afirma ser um momento. A classe recusa-se a fingir que data e hora sozinhas são informação suficiente; é um rótulo em um relógio de parede em algum lugar, e qual parede depende do fuso que você fornecer. - A verificação final de imutabilidade mostrou
nowinalterado apósplusDays(7).withHour(0).withMinute(0). Essa garantia se aplica a todas as operações, todos os encadeamentos, todos os helpers — não há como mutar umLocalDateTime. Passe-o livremente, compartilhe-o entre threads, armazene-o em umMap.
O que vem a seguir
LocalDateTime é o último dos três tipos "Local" — sem fuso, sem afirmar ser um momento. O próximo capítulo, Java ZonedDateTime, adiciona o fuso explicitamente: um LocalDateTime mais um ZoneId mais o offset resolvido para aquele horário local naquele fuso, que juntos fixam um momento real na linha do tempo global.