Formateo de fechas en Java
Formatea fechas y horas de Java en cadenas con DateTimeFormatter usando patrones estándar o personalizados.
El formateo de fechas consiste en convertir un valor de fecha/hora en una cadena legible por humanos. Este capítulo cubre DateTimeFormatter — cómo construir uno (integrado, localizado o basado en patrones), el alfabeto completo de patrones, el manejo de locale y zona horaria, y los errores comunes que producen resultados incorrectos. Funciona con todos los tipos de java.time: LocalDate, LocalTime, LocalDateTime, ZonedDateTime e Instant.
Cada tipo de java.time tiene un toString() que produce la representación ISO-8601: 2025-11-04, 14:30:00, 2025-11-04T14:30:00Z. Eso está bien para logs y tráfico máquina a máquina. Para mostrar información a humanos ("4 de noviembre de 2025" o "4 Nov, 14:30") necesitas un formateador.
La clase es java.time.format.DateTimeFormatter. Es el reemplazo moderno, thread-safe e inmutable del legado java.text.SimpleDateFormat (que no era thread-safe y causaba errores sutiles en producción cuando se compartía). Guárdalo en caché como static final y reutilízalo en todos los hilos para siempre — sin sincronización, sin copias defensivas.
Tres formas de construir un formateador
// 1. Built-in ISO formatters
DateTimeFormatter.ISO_LOCAL_DATE; // 2025-11-04
DateTimeFormatter.ISO_LOCAL_DATE_TIME; // 2025-11-04T14:30:00
DateTimeFormatter.ISO_OFFSET_DATE_TIME; // 2025-11-04T14:30:00-05:00
DateTimeFormatter.ISO_ZONED_DATE_TIME; // 2025-11-04T14:30:00-05:00[America/New_York]
DateTimeFormatter.ISO_INSTANT; // 2025-11-04T19:30:00Z
DateTimeFormatter.BASIC_ISO_DATE; // 20251104
// 2. Localised formatters
DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG); // November 4, 2025 (en-US)
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM); // Nov 4, 2025, 2:30:00 PM
// 3. Pattern-based formatters
DateTimeFormatter.ofPattern("dd MMM yyyy"); // 04 Nov 2025
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm zzz"); // 2025-11-04 14:30 ESTLa API de patrones es la que más usarás. La localizada es la correcta cuando necesitas un formato apropiado para una cultura y quieres que el JDK elija la disposición por ti.
Formateo
La forma de llamada es simétrica en ambos lados:
String s = formatter.format(temporal);
String s2 = temporal.format(formatter); // same thing, fluent styleAmbas funcionan. La mayoría del código usa la forma fluida.
LocalDate today = LocalDate.now();
String us = today.format(DateTimeFormatter.ofPattern("MM/dd/yyyy")); // 11/04/2025
String iso = today.format(DateTimeFormatter.ISO_LOCAL_DATE); // 2025-11-04
String eu = today.format(DateTimeFormatter.ofPattern("dd.MM.yyyy")); // 04.11.2025El alfabeto de patrones
La tabla principal — a la que volverás. Las letras distinguen mayúsculas y minúsculas y la cantidad importa.
| Letra | Significado | Ejemplo |
|---|---|---|
y | año | y → 2025, yy → 25, yyyy → 2025 |
M | mes | M → 11, MM → 11, MMM → Nov, MMMM → November |
d | día del mes | d → 4, dd → 04 |
E | día de la semana | E → Tue, EEEE → Tuesday |
H | hora 0-23 | H → 14, HH → 14 |
h | hora 1-12 | h → 2, hh → 02 (usar con a) |
a | AM/PM | a → PM |
m | minuto | m → 5, mm → 05 |
s | segundo | s → 9, ss → 09 |
S | fracción de segundo | SSS → 123 (millis) |
n | nanosegundo | nnnnnnnnn → 123456789 |
z | nombre de zona | z → EST, zzzz → Eastern Standard Time |
Z | desplazamiento de zona | Z → -0500, ZZ → -0500, ZZZZ → GMT-05:00 |
X | desplazamiento ISO | X → -05, XX → -0500, XXX → -05:00 |
V | ID de zona | VV → America/New_York |
El texto literal se escribe entre comillas simples:
DateTimeFormatter.ofPattern("EEEE, MMMM d 'at' h:mm a"); // Tuesday, November 4 at 2:30 PMPara una comilla simple literal, usa dos: ''.
El par que más confunde es m vs M (minúscula = minuto, mayúscula = mes) y H vs h (mayúscula = 0-23, minúscula = 1-12). La mayoría de los errores del tipo "la hora está mal por algo extraño" provienen de uno de estos errores tipográficos.
Localización: Locale y withLocale
Un formateador toma el locale predeterminado de la JVM a menos que le indiques lo contrario. Para una salida "siempre en inglés" o "siempre en alemán", fija el locale:
DateTimeFormatter english = DateTimeFormatter.ofPattern("EEEE, MMMM d", Locale.US);
DateTimeFormatter german = DateTimeFormatter.ofPattern("EEEE, d. MMMM", Locale.GERMAN);
DateTimeFormatter french = DateTimeFormatter.ofPattern("EEEE d MMMM", Locale.FRENCH);
today.format(english); // Tuesday, November 4
today.format(german); // Dienstag, 4. November
today.format(french); // mardi 4 novembrePara contenido renderizado en el servidor, siempre pasa un locale. El "locale predeterminado de la JVM" es impredecible en servidores de producción y es la fuente de errores del tipo "funciona bien en mi laptop, pero mal en el servidor".
Visualización de zona horaria
ZonedDateTime e Instant son los únicos tipos que tienen información de zona. Formatear un LocalDateTime con un patrón que incluye z o Z lanza una excepción — no hay zona que imprimir. Convierte primero:
ZonedDateTime zdt = ldt.atZone(ZoneId.of("America/New_York"));
zdt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm z")); // 2025-11-04 14:30 ESTPara Instant, el formateador también necesita una zona — Instant no tiene ninguna, por lo que los formateadores de visualización que incluyen cualquier campo dependiente de zona necesitan un withZone:
DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
.withZone(ZoneId.of("America/New_York"));
f.format(Instant.now()); // formatter supplies the zone for displaySin withZone, formatear un Instant con un patrón con forma de calendario lanza una excepción.
Formateadores con estilo mediante FormatStyle
Las fábricas localizadas ofrecen cuatro tamaños canónicos:
DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT); // 11/4/25 (en-US), 04.11.25 (de-DE)
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM); // Nov 4, 2025
DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG); // November 4, 2025
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL); // Tuesday, November 4, 2025Los mismos cuatro tamaños existen para ofLocalizedTime y ofLocalizedDateTime. Úsalos cuando quieras que la disposición siga el locale del usuario en lugar de imponer una forma fija. Combínalos con .withLocale(...) para fijar el locale.
Un ejemplo práctico: una fecha, seis variantes de visualización
El programa a continuación formatea un ZonedDateTime de seis maneras comunes: ISO para logs de máquina, formato US de 12 horas para usuarios en inglés, formato europeo de 24 horas para usuarios alemanes, una forma larga localizada, un patrón personalizado con texto literal incrustado, y un formateador Instant-mediante-withZone para marcas de tiempo sin procesar.
Lo que debes extraer de la ejecución:
- Los campos
static final DateTimeFormatteren caché tienen la forma correcta.DateTimeFormatteres inmutable y thread-safe; crear uno es barato pero no gratuito, y reutilizar la misma instancia en todas partes es el patrón recomendado por el JDK. No construyas uno nuevo dentro de un bucle muy ejecutado. - El mismo
ZonedDateTimeprodujo seis cadenas diferentes dependiendo del formateador. El objeto de valor nunca cambió; el formateador es lo único que controla la disposición. Esa es la separación para la que existeDateTimeFormatter— mantén el tipo de valor limpio y delega la presentación al formateador. - El bloque de "errores tipográficos comunes" imprimió
14:11paraHH:MMporqueMes mes, no minuto. Los dos son el par más confuso del alfabeto de patrones. Si la hora que se muestra parece sospechosamente un componente de fecha, revisa las mayúsculas del patrón. - La escala de
FormatStyleprodujo cuatro cadenas progresivamente más largas. UsaFormatStyle.MEDIUMcomo valor predeterminado razonable para "mostrar una fecha a un usuario sin pensar demasiado";LONGyFULLpara contextos donde el año y el día de la semana deben ser inequívocos;SHORTpara espacios de UI ajustados. LocalDateTimecon un patrón con zona lanzó una excepción — el formateador necesita datos de zona, yLocalDateTimeno tiene ninguna. La solución es convertir (ldt.atZone(zone)) o eliminar el campo con zona del patrón. De cualquier manera, el modo de fallo es claro en tiempo de ejecución.
¿Qué sigue?
El formateo es la dirección valor → string. El siguiente capítulo, Análisis de fechas en Java, es la operación inversa — string → valor — usando los mismos patrones de DateTimeFormatter y el mismo conjunto de advertencias. Los dos juntos forman el límite de E/S para cualquier código que intercambie fechas con usuarios, configuraciones, logs o APIs remotas.