Día 7 de 30 Fase 1: Java Vanilla — El Núcleo del Lenguaje (17 & 21)

APIs del Día a Día: Fechas, Texto, Ficheros, HTTP y Logging

java.time, StringBuilder y expresiones regulares, NIO con Files y Path, HttpClient y logging con SLF4J y Logback

Para dominar. Al terminar deberías poder hacerlo sin ayuda.

  • 3.5h de estudio
  • 5 conceptos
  • 3 preguntas
  • 2 katas
Java 17 LTSJava 21 LTSjava.timeRegexNIOHttpClientSLF4J
Al terminar este día sabrás 4
  • Elegir entre Instant, LocalDate, LocalDateTime y ZonedDateTime según lo que representa el dato.
  • Construir y validar texto con StringBuilder, text blocks y expresiones regulares compiladas una vez.
  • Leer y escribir ficheros con Path y Files sin fugas de recursos.
  • Llamar a una API con HttpClient con timeouts y registrar los errores con SLF4J sin perder el stack trace.

Fechas y Horas con java.time

LocalDate, LocalTime y LocalDateTime representan fechas sin zona horaria; ZonedDateTime e Instant representan un momento exacto. Todas son inmutables. Duration mide tiempo en segundos y Period en días, meses y años.

En una línea Instant.now(), LocalDate.of(2026, 10, 28).plusDays(30), Duration.between(inicio, fin).toMillis().

Instant para registrar, zona para mostrar y Clock para probar java
public class Vencimientos {
    private final Clock reloj;

    public Vencimientos(Clock reloj) { this.reloj = reloj; }

    public Instant fechaLimite(Duration plazo) {
        return Instant.now(reloj).plus(plazo);                 // momento exacto, en UTC
    }

    public String paraUsuario(Instant momento, ZoneId zona) {
        return DateTimeFormatter.ofPattern("d 'de' MMMM, HH:mm", Locale.forLanguageTag("es"))
            .format(momento.atZone(zona));                     // se convierte solo al mostrar
    }
}

// En el test: tiempo fijo y resultado reproducible
var reloj = Clock.fixed(Instant.parse("2026-10-28T10:00:00Z"), ZoneOffset.UTC);
var v = new Vencimientos(reloj);

assertThat(v.fechaLimite(Duration.ofHours(48)))
    .isEqualTo(Instant.parse("2026-10-30T10:00:00Z"));
assertThat(v.paraUsuario(Instant.parse("2026-10-28T10:00:00Z"), ZoneId.of("America/Mexico_City")))
    .isEqualTo("28 de octubre, 04:00");
Cómo funciona por dentro

java.time separa dos ideas que Date mezclaba: el momento exacto y su representación humana. Instant es un punto en la línea del tiempo (segundos y nanosegundos desde 1970 en UTC): ideal para registrar cuándo ocurrió algo. LocalDate, LocalTime y LocalDateTime son fechas y horas «de calendario» sin zona: un cumpleaños, la hora de apertura de una tienda. ZonedDateTime y OffsetDateTime unen una fecha local a una zona o a un desfase, y sí representan un momento exacto.

Todas las clases son inmutables y seguras entre hilos: plusDays devuelve un objeto nuevo. Duration mide intervalos en segundos (útil para timeouts), Period en unidades de calendario (un mes no siempre tiene los mismos días) y DateTimeFormatter convierte a texto y desde texto.

Para poder probar código que depende de la hora, no llames a Instant.now() directamente: recibe un Clock por constructor. En producción inyectas Clock.systemUTC() y en los tests Clock.fixed(...), y el resultado deja de depender del momento en que se ejecuta el test.

Las fechas son una fuente constante de bugs: zonas horarias, cambios de hora y formatos. java.time sustituye a Date y Calendar, que eran mutables y confusos.

Se conecta con Instant en APIs RESTZonas horariasClock en los tests

Compruébalo: ¿Qué tipo usarías para la fecha de nacimiento de un cliente?

LocalDate. Es una fecha de calendario que no depende de la zona horaria: alguien nacido el 3 de mayo cumple años el 3 de mayo esté donde esté.

Cuatro bloques cronometrados. No pases al siguiente sin tener el resultado esperado.

  1. Bloque 1: Fechas sin Sorpresas (0 - 25 min)

    Qué estudiar. Instant, LocalDate, ZonedDateTime, Duration, Period y Clock.

    Pregunta a responder. ¿Qué tipo usarías para una fecha de nacimiento y cuál para la hora de un pago?

    Resultado esperado. Clase CalculadoraVencimientos que recibe un Clock, con tests que fijan el tiempo.

  2. Bloque 2: Texto y Expresiones Regulares (25 - 45 min)

    Qué estudiar. StringBuilder, text blocks, Pattern y grupos con nombre.

    Pregunta a responder. ¿Por qué hay que compilar la regex una sola vez?

    Resultado esperado. Validador de matrículas españolas con Pattern y tests de casos válidos e inválidos.

  3. Bloque 3: Ficheros, HTTP y Logs (45 - 80 min)

    Qué estudiar. Files.lines, HttpClient con timeouts y SLF4J con marcadores.

    Pregunta a responder. ¿Cómo registrar un error sin perder el stack trace?

    Resultado esperado. Programa que lee un CSV, llama a una API pública por cada fila con HttpClient y registra los errores con SLF4J.

  4. Cierre de Consolidación (80 - 90 min)

    Qué estudiar. Repaso de la rúbrica del tutor.

    Pregunta a responder. Explicar sin mirar la diferencia entre Instant y LocalDateTime.

    Resultado esperado. Lista de las tres APIs del JDK que más vas a usar en tu proyecto y para qué.

Tres formas de comprobar si lo has entendido, de menos a más exigente: explicarlo con tus palabras, responder preguntas trampa y resolver el reto del tutor.

Explícalo desde cero

Lee cada ficha y explícala en voz alta antes de seguir: si puedes contárselo a alguien que empieza, lo has entendido.

La diferencia entre LocalDateTime e Instant

Lo que debes entender

LocalDateTime es lo que marca un reloj de pared sin saber dónde está; Instant es un punto exacto en la línea del tiempo, igual en todo el mundo. Para registrar cuándo pasó algo, Instant. Para una cita en el calendario de una persona, fecha local más su zona.

Analogía para contarlo

«Reunión a las 10:00» no basta si un asistente está en Madrid y otro en Lima. Hace falta saber de qué 10:00 hablas.

Lo que suele explicarse mal

Decir que LocalDateTime es «la hora local del servidor». No tiene zona: es solo una fecha y una hora sin contexto.

Señal de que aún no lo dominas

Si guardas fechas de eventos con LocalDateTime y tu aplicación tiene usuarios en varias zonas horarias.

Preguntas trampa

Responde de memoria, en voz alta o por escrito, y después abre la respuesta razonada: lo importante es el porqué, no la palabra exacta.

Nivel 1 (Básico) ¿Por qué concatenar cadenas con + dentro de un bucle de 100.000 vueltas es mala idea? Ver respuesta

Respuesta razonada

Porque cada + crea una cadena nueva y copia en ella todos los caracteres anteriores. En la vuelta 1.000 copia 1.000 trozos y en la 100.000 copia 100.000: el coste total crece con el cuadrado del número de vueltas y genera millones de objetos temporales para el recolector.

StringBuilder añade al final de un búfer que duplica su capacidad cuando se llena, así que el coste total es lineal. Fuera de bucles no hace falta: el compilador ya traduce "a" + b + "c" a una concatenación optimizada (con invokedynamic desde Java 9).

Qué está evaluando la pregunta

Porque String es inmutable: cada + crea una cadena nueva y copia la anterior, con un coste que crece de forma cuadrática. StringBuilder reutiliza un búfer interno. Fuera de bucles, el compilador ya optimiza las concatenaciones simples.

Nivel 2 (Intermedio) Un servicio llama a una API externa con HttpClient y a veces se queda colgado minutos. ¿Qué falta? Ver respuesta

Respuesta razonada

Le faltan timeouts. Sin connectTimeout, si el servidor no acepta conexiones el hilo espera lo que decida el sistema operativo; sin timeout en la petición, si el servidor acepta y no responde, espera indefinidamente. Cada petición colgada ocupa un hilo, y con suficientes el servicio entero deja de responder.

La solución completa: timeouts en los dos niveles, reintentos acotados solo para errores transitorios y operaciones idempotentes, y un circuit breaker que deje de llamar a la API si falla de forma continuada. Y registrar cada timeout para saber que está pasando.

Qué está evaluando la pregunta

Timeouts. connectTimeout en el cliente limita el tiempo de conexión y timeout en la petición (HttpRequest.newBuilder().timeout(...)) el tiempo total de respuesta. Sin ellos el hilo espera indefinidamente. En producción se combina con reintentos y circuit breaker.

Nivel 3 (Entrevista Senior) ¿Qué problema tiene log.debug("Pedido: " + pedido) en un servicio con mucho tráfico, aunque el nivel DEBUG esté desactivado? Ver respuesta

Respuesta razonada

El argumento de log.debug se evalúa antes de llamar al método: Java construye la cadena y llama al toString() del pedido (que puede recorrer colecciones o disparar relaciones LAZY) aunque el logger la descarte al ver que DEBUG está desactivado. Con miles de peticiones por segundo es CPU y basura desperdiciadas.

Con log.debug("Pedido: {}", pedido) solo se pasa la referencia y el formateo ocurre únicamente si el nivel está activo. Si calcular el argumento es caro en sí mismo, se envuelve en un if (log.isDebugEnabled()) o se usa la API fluida de SLF4J 2 con un Supplier.

Qué está evaluando la pregunta

La concatenación y el toString() se ejecutan siempre, antes de que el logger compruebe el nivel. Con marcadores (log.debug("Pedido: {}", pedido)) el mensaje solo se formatea si el nivel está activo. Si el argumento es caro de calcular, SLF4J 2 permite pasarlo como Supplier con la API fluida (log.atDebug().addArgument(() -> calcular())).

Reto del tutor

Un compañero guarda en la base de datos la fecha de cada pedido con LocalDateTime.now(). La aplicación se va a desplegar en servidores de Irlanda y tiene clientes en México. Evalúa la decisión.

Lo que una respuesta suele tener bien

Usa java.time en lugar de Date.

Lo que suele quedarse en la superficie

Decir «hay que usar UTC» sin explicar dónde ni cómo.

El concepto de fondo que suele faltar

LocalDateTime.now() toma la zona del servidor y la descarta: el dato no indica en qué zona se generó. Si cambian los servidores o su configuración, los datos dejan de ser comparables, y en los cambios de hora puede haber horas repetidas.

Cómo lo respondería alguien senior en una entrevista

Para el momento en que ocurre algo se guarda un Instant (o un OffsetDateTime en UTC, en una columna timestamp with time zone) y se convierte a la zona del usuario solo al mostrarlo. Además se inyecta un Clock en vez de llamar a now() directamente, para poder fijar el tiempo en los tests.

Proyecto «Pedidos» · paso 7 de 30

Importar el catálogo y registrar lo que pasa

Cargar datos reales desde un fichero, trabajar con fechas sin ambigüedad y dejar rastro en los logs.

De qué va el proyecto

El backend de una tienda online: clientes, catálogo de productos y pedidos con sus líneas, pagos y envíos. Empieza como un proyecto Maven con records y termina como un sistema con API REST, seguridad, mensajería, contenedores y observabilidad. Cada paso construye sobre el anterior, así que trabaja siempre en el mismo repositorio.

Qué hacer

  1. Importa el catálogo desde catalogo.csv con Files.lines, descartando las líneas cuyo sku no cumpla un Pattern.
  2. Guarda la fecha de creación de cada pedido como Instant obtenido de un Clock inyectado.
  3. Registra con SLF4J cada pedido confirmado (INFO) y cada línea descartada del CSV (WARN).

Está listo cuando

  • Los tests fijan el reloj con Clock.fixed y no dependen de la hora a la que se ejecutan.
  • Un CSV con líneas erróneas se importa igual y el log dice cuántas se descartaron.

Katas: ejercicios cortos

Reglas de oro

  • Para registrar cuándo pasó algo, Instant; para mostrarlo, conviértelo a la zona del usuario.
  • Compila las expresiones regulares una vez, en un campo static final.
  • Todo HttpClient lleva timeouts.
  • En los logs, marcadores {} y la excepción como último argumento.

Java 17 frente a Java 21

CaracterísticaJava 17 LTSJava 21 LTS
Text blocks Sí (desde Java 15) Sí
Charset por defecto Depende del sistema operativo UTF-8 siempre (JEP 400, desde Java 18)
StringBuilder.repeat No Sí
Se guarda automáticamente

Fuentes oficiales

Revisado el . Este día en Markdown