# Día 2: Cimientos del Lenguaje, POO Limpia y Records Inmutables

> Primitivos vs Wrappers, Modificadores de Acceso y Modelado de Datos en Java 17 y 21

- Plan: [JavaMastery](https://aprende-java.webs-economicas.es/), día 2 de 30
- Fase 1: Java Vanilla — El Núcleo del Lenguaje (17 & 21)
- Dedicación: 3h
- Profundidad: para dominar
- Revisado: 2026-10-29
- Versión interactiva: https://aprende-java.webs-economicas.es/dias/poo-limpia-records/

## Al terminar este día sabrás

- Explicar dónde vive cada valor en memoria y por qué `==` falla con wrappers.
- Diseñar clases que protegen sus invariantes con métodos de negocio en lugar de setters.
- Escribir records con validación, copias defensivas y métodos propios.
- Usar `var` y los modificadores de acceso con criterio, no por costumbre.


## Conceptos

### 1. Primitivos vs Wrappers: Almacenamiento Directo frente a Objetos en Heap

Los tipos primitivos (int, double, boolean) guardan su valor numérico directamente en el Stack. Los wrappers (Integer, Double) son objetos completos que viven en el Heap, ocupan más memoria y pueden ser nulos (null).

**Cómo funciona por dentro.**

Un `int` ocupa 4 bytes y su valor se guarda directamente donde se declara: en el marco de pila del método si es variable local, o dentro del objeto si es un campo. Un `Integer` es un objeto en el heap con cabecera (unos 12-16 bytes) más el valor; la variable solo guarda una referencia. Por eso un `List<Integer>` de un millón de elementos ocupa varias veces más que un `int[]` equivalente.

El autoboxing convierte automáticamente entre ambos mundos. `Integer x = 5` se compila como `Integer.valueOf(5)`, que devuelve objetos cacheados para el rango -128 a 127. Fuera de ese rango cada llamada crea un objeto nuevo, y comparar con `==` compara referencias, no valores. El unboxing es la conversión inversa: si el wrapper es `null`, desempaquetarlo lanza `NullPointerException`, un error frecuente al leer un `Map<String, Integer>`.

Regla práctica: primitivos para cálculo y contadores; wrappers solo cuando necesitas `null` como valor legítimo o cuando la API lo exige (genéricos, colecciones). Compara wrappers siempre con `equals` u `Objects.equals`.

*Las tres trampas del autoboxing*

```java
Integer a = 127, b = 127;
System.out.println(a == b);        // true: mismo objeto del caché
Integer c = 128, d = 128;
System.out.println(c == d);        // false: dos objetos distintos
System.out.println(c.equals(d));   // true: compara el valor

// Trampa 2: unboxing de null
Map<String, Integer> stock = new HashMap<>();
int unidades = stock.get("teclado"); // NullPointerException al desempaquetar
int seguras = stock.getOrDefault("teclado", 0); // correcto

// Trampa 3: boxing dentro de un bucle caliente
Long total = 0L;                   // wrapper: crea un objeto por iteración
for (long i = 0; i < 1_000_000; i++) {
    total += i;                    // unbox, suma, box
}
long rapido = 0L;                  // primitivo: sin objetos intermedios
```

- **En una línea:** `int edad = 25;` (directo y rápido) vs `Integer edadWrapper = Integer.valueOf(25);`
- **Por qué importa:** Permite optimizar el rendimiento y evitar NullPointerException al desempaquetar valores numéricos.
- **Error típico:** Usar `Integer` para contadores masivos o comparar wrappers con `==` esperando que compare el número.
- **Java 17 frente a 21:** Idéntico en Java 17 y Java 21: las colecciones como `List<Integer>` siguen requiriendo wrappers.
- **Se conecta con:** Memoria Stack vs Heap, Autoboxing y Unboxing, Colecciones Genéricas

### 2. Encapsulamiento con Intención: Métodos de Negocio en vez de Setters Ciegos

Encapsular no es crear getters y setters para todo; es proteger los invariantes de la clase y exponer métodos que reflejen acciones reales del dominio.

**Cómo funciona por dentro.**

Un invariante es una regla que debe cumplirse siempre que el objeto sea observable: el saldo nunca es negativo, una tarea completada tiene fecha de cierre, un pedido enviado no admite nuevas líneas. Encapsular consiste en que sea imposible romper esas reglas desde fuera.

Los setters genéricos trasladan la responsabilidad a quien llama: cualquiera puede poner el objeto en un estado inválido o a medio actualizar. Los métodos de negocio (`completar()`, `retirar(importe)`) concentran la validación en un solo sitio, expresan la intención del dominio y permiten cambiar la representación interna sin tocar el resto del código.

Esto no significa prohibir todos los getters. Exponer datos para leerlos es razonable; lo que se evita es exponer la mutación sin control. Si una clase es solo una bolsa de datos sin reglas, probablemente debería ser un `record`.

*Tarea con invariantes protegidos*

```java
public class Tarea {
    private final Long id;
    private String titulo;
    private Estado estado = Estado.PENDIENTE;
    private Instant completadaEn;

    public Tarea(Long id, String titulo) {
        this.id = id;
        renombrar(titulo);                 // reutiliza la validación
    }

    public void renombrar(String nuevoTitulo) {
        if (nuevoTitulo == null || nuevoTitulo.isBlank()) {
            throw new IllegalArgumentException("El título es obligatorio");
        }
        this.titulo = nuevoTitulo.strip();
    }

    public void completar(Clock reloj) {
        if (estado == Estado.COMPLETADA) {
            throw new IllegalStateException("La tarea ya estaba completada");
        }
        this.estado = Estado.COMPLETADA;
        this.completadaEn = Instant.now(reloj); // estado y fecha cambian juntos
    }

    public String titulo() { return titulo; }
    public boolean estaCompletada() { return estado == Estado.COMPLETADA; }

    enum Estado { PENDIENTE, COMPLETADA }
}
```

- **En una línea:** `tarea.marcarComoCompletada()` en lugar de `tarea.setCompletada(true)`.
- **Por qué importa:** Evita que el estado del objeto quede corrupto o en estados intermedios inválidos desde otras clases.
- **Error típico:** Poner setters públicos a todos los campos y permitir que cualquiera modifique campos sin validación.
- **Java 17 frente a 21:** Aplica a cualquier versión de Java. En 17 y 21 se complementa con `record` para datos puros.
- **Se conecta con:** Inmutabilidad, Constructores Canónicos, Clean Code

### 3. Records de Java: Inmutabilidad Garantizada por el Compilador

Una clase de datos inmutable donde el compilador genera automáticamente constructor, getters, equals(), hashCode() y toString().

**Cómo funciona por dentro.**

Un `record` declara sus componentes en la cabecera y el compilador genera campos `private final`, un constructor canónico, accesores con el mismo nombre que el componente (`titulo()`, no `getTitulo()`), y `equals`, `hashCode` y `toString` basados en todos los componentes. Es implícitamente `final` y extiende `java.lang.Record`.

El constructor compacto permite validar y normalizar sin repetir asignaciones: escribes la lógica y el compilador añade las asignaciones al final. También puedes añadir métodos de instancia, métodos estáticos de fábrica e implementar interfaces. Lo que no puedes es añadir campos de instancia extra ni heredar de otra clase.

La inmutabilidad de un record es superficial: los campos no se pueden reasignar, pero si un componente es una lista mutable, su contenido sí puede cambiar. Por eso, para componentes de colección, haz una copia defensiva con `List.copyOf` en el constructor compacto.

*Record con validación y copia defensiva*

```java
public record TareaDto(Long id, String titulo, List<String> etiquetas) {

    // Constructor compacto: valida y normaliza; las asignaciones las añade el compilador
    public TareaDto {
        Objects.requireNonNull(titulo, "titulo");
        if (titulo.isBlank()) throw new IllegalArgumentException("titulo vacío");
        titulo = titulo.strip();
        etiquetas = List.copyOf(etiquetas);   // copia inmutable: corta el vínculo con la lista original
    }

    // Fábrica con nombre expresivo
    public static TareaDto nueva(String titulo) {
        return new TareaDto(null, titulo, List.of());
    }

    // Los records no tienen setters: se crea una copia modificada ("wither")
    public TareaDto conTitulo(String nuevo) {
        return new TareaDto(id, nuevo, etiquetas);
    }
}

var t = TareaDto.nueva("  Leer JEP 395 ");
System.out.println(t.titulo());   // "Leer JEP 395"
System.out.println(t);            // TareaDto[id=null, titulo=Leer JEP 395, etiquetas=[]]
```

- **En una línea:** `public record TareaDto(Long id, String titulo, boolean completada) {}`
- **Por qué importa:** Elimina cientos de líneas de código repetitivo sin librerías externas como Lombok.
- **Error típico:** Intentar usar un `record` para una entidad mutable de base de datos JPA (JPA requiere proxies mutables).
- **Java 17 frente a 21:** Estándar en Java 17. En Java 21 se pueden desestructurar en sentencias switch con pattern matching.
- **Se conecta con:** DTOs de API, Contrato equals/hashCode, Programación Concurrente Thread-Safe

**Compruébalo:** ¿Qué pasa si en el constructor compacto escribes `this.titulo = titulo.strip();`?

No compila. En el constructor compacto los campos se asignan automáticamente al final, así que no puedes asignarlos tú. Se reasigna el parámetro (`titulo = titulo.strip();`) y el compilador copia ese valor al campo.

### 4. Modificadores de Acceso y el Principio de Menor Privilegio

Java tiene 4 niveles de visibilidad: private, default (package-private), protected y public. Cada campo debe ser lo más privado posible.

**Cómo funciona por dentro.**

Java tiene cuatro niveles de visibilidad, de más a menos restrictivo: `private` (solo la propia clase), sin modificador o package-private (la clase y su paquete), `protected` (el paquete más las subclases, estén donde estén) y `public` (todo el mundo).

El nivel package-private está infravalorado. Permite agrupar en un paquete por funcionalidad (`tareas`) el controlador, el servicio y el repositorio, y dejar públicas solo las clases que otros paquetes deben usar. El resto queda como detalle de implementación que puedes cambiar sin miedo. Spring funciona sin problema con clases y constructores package-private.

El principio de menor privilegio dice: empieza con lo más restrictivo y abre solo cuando haga falta. Cada miembro `public` es una promesa que otros pueden empezar a usar y que te costará retirar. `protected` es aún más delicado porque expone detalles internos a una jerarquía que no controlas.

*Paquete por funcionalidad con API mínima*

```java
// com/acme/tareas/TareaService.java
package com.acme.tareas;

public class TareaService {                 // público: lo usan otros paquetes
    private final TareaRepository repo;      // privado: detalle interno

    TareaService(TareaRepository repo) {     // package-private: Spring lo inyecta igual
        this.repo = repo;
    }

    public TareaDto crear(String titulo) {   // la única operación pública
        var tarea = new Tarea(null, titulo);
        return TareaMapper.aDto(repo.guardar(tarea));
    }
}

// com/acme/tareas/TareaMapper.java
package com.acme.tareas;

final class TareaMapper {                    // invisible fuera de com.acme.tareas
    private TareaMapper() {}
    static TareaDto aDto(Tarea t) { /* ... */ return null; }
}
```

- **En una línea:** `private final String titulo;` accesible solo por métodos internos.
- **Por qué importa:** Oculta los detalles internos de implementación para que cambiar el interior de una clase no rompa el resto del sistema.
- **Error típico:** Declarar atributos con visibilidad `public` o `protected` por comodidad en lugar de privados.
- **Java 17 frente a 21:** Idéntico en Java 17 y 21.
- **Se conecta con:** Arquitectura Modular, Encapsulamiento, Inyección por Constructor

### 5. Inferencia de Tipos con `var`: Tipado Estático Inmutable

`var` permite que el compilador deduzca el tipo de una variable local sin tener que repetirlo a ambos lados de la asignación.

**Cómo funciona por dentro.**

`var` solo existe para variables locales, índices de bucles `for` y recursos de try-with-resources. El compilador infiere el tipo a partir del inicializador en tiempo de compilación y lo fija para siempre: no puedes asignar después un valor de otro tipo. No se puede usar en campos, parámetros ni tipos de retorno, y exige inicializador (`var x;` no compila, `var x = null;` tampoco).

Usa `var` cuando el tipo es evidente a la derecha (`new`, fábricas con nombre claro) o cuando el tipo completo es ruido (`Map.Entry<String, List<Tarea>>` en un bucle). Evítalo cuando el lector no puede deducir el tipo (`var resultado = servicio.procesar();`) o con literales numéricos ambiguos.

Cuidado con el diamante: `var lista = new ArrayList<>();` infiere `ArrayList<Object>`, porque no hay tipo destino del que deducir el genérico.

*Cuándo ayuda y cuándo confunde*

```java
// Bien: el tipo es obvio a la derecha
var tareas = new ArrayList<TareaDto>();
var porEstado = new HashMap<Estado, List<TareaDto>>();

for (var entrada : porEstado.entrySet()) {     // evita Map.Entry<Estado, List<TareaDto>>
    System.out.println(entrada.getKey() + " -> " + entrada.getValue().size());
}

// Mal: el lector no sabe qué tipo es
var r = servicio.procesar(peticion);

// Trampa del diamante
var lista = new ArrayList<>();      // ArrayList<Object>
lista.add("texto");
lista.add(42);                      // compila: el tipo es Object

// Trampa numérica
var total = 0;                      // int, no long
total += 3_000_000_000L;            // compila, pero desborda en silencio (conversión implícita del +=)
```

- **En una línea:** `var tareas = new ArrayList<TareaDto>();` (el tipo es exactamente ArrayList).
- **Por qué importa:** Hace el código más limpio y legible cuando el tipo a la derecha es obvio.
- **Error típico:** Creer que `var` convierte a Java en un lenguaje de tipado dinámico; el tipo se fija al compilar.
- **Java 17 frente a 21:** Disponible desde Java 10, completamente maduro en Java 17 y 21.
- **Se conecta con:** Compilador javac, Tipado Estático, Legibilidad de Código


## Sesión de 90 minutos

### Bloque 1: Cimientos y Primitivos (0 - 25 min)

- **Qué estudiar:** Diferencia de memoria Stack vs Heap. Primitivos vs Wrappers. Caché de Integer.
- **Pregunta a responder:** ¿Por qué desempaquetar un `Long` nulo lanza NullPointerException y cómo evitarlo?
- **Entregable:** Clase ejecutable probando los límites del Integer Cache (-128 a 127) y boxing.

### Bloque 2: Inmutabilidad y Records (25 - 50 min)

- **Qué estudiar:** Sintaxis de `record` en Java 17 y constructor compacto para validar invariantes.
- **Pregunta a responder:** ¿Cómo garantizar que un record con una lista interna sea 100% inmutable?
- **Entregable:** Record `UsuarioDto` con constructor compacto que use `List.copyOf()` para defensa contra mutaciones.

### Bloque 3: Diferencias Java 17 vs Java 21 (50 - 80 min)

- **Qué estudiar:** Pattern Matching para instanceof en Java 17 frente a Record Patterns en Java 21.
- **Pregunta a responder:** ¿Cómo se desestructura un record en Java 21 frente a Java 17?
- **Entregable:** Snippet de código comparando la extracción en Java 17 con `record.campo()` y en 21 con desestructuración directa.

### Cierre y Consolidación (80 - 90 min)

- **Qué estudiar:** Repaso activo de los 5 conceptos centrales sin mirar las notas.
- **Pregunta a responder:** Responder de memoria las 4 preguntas anti-memorización.
- **Entregable:** Resumen en el cuaderno de notas personal de la app con los 3 aprendizajes clave de hoy.


## Explícalo desde cero

### La diferencia real entre Stack y Heap para primitivos y objetos

- **Qué entender:** El Stack guarda los valores inmediatos de las variables de método. Si es un primitivo (int), guarda el número; si es un objeto, guarda el puntero de memoria hacia el Heap.
- **Qué suele explicarse mal:** Decir que los primitivos "son más rápidos" sin explicar que evitan indirecciones de punteros y recolección de basura.
- **Ejemplo sencillo:** El Stack es como un bloc de notas inmediato en tu mano; el Heap es como un almacén grande donde guardas cajas y en tu bloc solo anotas el número de estantería.
- **Señal de alerta:** Si dices que `Integer a = 10;` guarda el 10 directamente en el hilo sin crear un objeto en memoria.

### Por qué los Records son inmutables por diseño

- **Qué entender:** Un `record` no tiene setters porque sus campos son implícitamente `private final`. Una vez creado, jamás cambia su estado interno.
- **Qué suele explicarse mal:** Explicarlo como "un simple DTO de Lombok" en lugar de un tipo formal del compilador Java con semántica matemática de tupla nombrada.
- **Ejemplo sencillo:** Un billete de avión: tiene tu nombre, vuelo y asiento impresos. Si cambias de vuelo, no borras el billete con típex; la aerolínea emite un billete nuevo.
- **Señal de alerta:** Si intentas crear un método setter dentro de un `record` o no sabes qué es un constructor compacto.


## Preguntas

### Nivel 1 (Básico): ¿Por qué `Integer a = 127; Integer b = 127; a == b;` devuelve `true`, pero con 128 devuelve `false`?

Expone si entiendes el Integer Cache (-128 a 127) implementado por la JVM para ahorrar memoria, y por qué los wrappers JAMÁS se comparan con `==`.

**Respuesta razonada:** `Integer a = 127` se compila como `Integer.valueOf(127)`. Ese método devuelve instancias cacheadas para valores entre -128 y 127 (el límite superior se puede ampliar con `-XX:AutoBoxCacheMax`). Por eso `a` y `b` apuntan al mismo objeto y `==`, que compara referencias, devuelve `true`.

Con 128 cada `valueOf` crea un objeto distinto y `==` devuelve `false`. La lección no es memorizar el rango, sino que `==` en objetos compara identidad. Para wrappers usa `equals` o, si pueden ser `null`, `Objects.equals(a, b)`.

### Nivel 2 (Intermedio): ¿Por qué un `record` en Java no puede extender de otra clase con `extends`?

Expone si sabes que todos los records extienden implícitamente de `java.lang.Record` y en Java no existe la herencia múltiple de clases.

**Respuesta razonada:** Todo record extiende implícitamente `java.lang.Record`, y Java solo permite heredar de una clase. Además, el diseño es intencionado: un record es un portador transparente de datos cuyo estado está definido solo por sus componentes. Si heredara campos de un padre, `equals`, `hashCode` y `toString` dejarían de describir el estado completo.

Lo que sí puede hacer un record es implementar interfaces, incluidas interfaces `sealed`, que es la forma idiomática de modelar jerarquías de datos en Java 17 y 21.

### Nivel 3 (Avanzado): Si los campos de un `record` son inmutables (`final`), ¿por qué un `record Tarea(List<String> etiquetas)` puede no ser inmutable en la práctica?

Expone la diferencia crítica entre referencia inmutable y contenido inmutable: si pasas un `ArrayList` mutable al record, cualquiera desde afuera puede hacer `etiquetas.add()`.

**Respuesta razonada:** El campo `etiquetas` es `final`, así que no puedes hacer que apunte a otra lista. Pero la lista a la que apunta puede ser un `ArrayList` mutable que el llamador conserva: si después hace `etiquetas.add("urgente")`, el record cambia sin que nadie lo haya tocado. Además, el `hashCode` del record cambia, lo que rompe su uso como clave de un `HashMap`.

La solución es copiar de forma inmutable en el constructor compacto: `etiquetas = List.copyOf(etiquetas);`. `List.copyOf` también rechaza `null` dentro de la lista y no copia si ya recibe una lista inmutable.

### Nivel 4 (Entrevista Senior): En un puesto que exige Java 17, un desarrollador propone migrar DTOs a records. ¿Qué consideración crítica debe tener si esos DTOs se serializan con Jackson o se usan como entidades JPA?

En Java 17 Jackson necesita la versión adecuada para mapear records sin constructor vacío, y en JPA Hibernate exige proxies mutables, por lo que los records no pueden ser entidades `@Entity`.

**Respuesta razonada:** Con Jackson: desde la versión 2.12 Jackson deserializa records usando el constructor canónico, sin necesidad de constructor vacío ni setters, y Spring Boot 3 ya trae una versión compatible. Conviene revisar que no haya configuraciones antiguas que dependan de setters o `@JsonProperty` en campos.

Con JPA: un record no puede ser `@Entity`. Hibernate necesita un constructor sin argumentos, campos no `final` para rellenarlos tras la consulta y la posibilidad de crear subclases proxy para la carga perezosa, y un record es `final` con campos `final`. Los records sí sirven como proyecciones de consultas (`select new com.acme.TareaResumen(t.id, t.titulo) ...`) y como DTOs de la API, que es exactamente donde deben estar.


## Reto del tutor

**Reto:** Un desarrollador júnior crea la clase `CuentaBancaria` con campos `public double saldo;` y añade getters y setters para todo. Explica por qué este diseño es deficiente y cómo corregirlo usando principios de Java 17 / 21.

- **Lo que está bien:** Identifica que hacer los campos públicos o tener setters abiertos permite asignar saldos negativos desde cualquier lugar.
- **Respuesta superficial:** Decir simplemente "falta poner private a los campos y usar getters/setters", porque un setter `setSaldo(saldo)` sigue permitiendo poner saldo en -5000 sin validar.
- **Lo que falta de fondo:** No entender que una cuenta bancaria es un objeto con estado de negocio mutable controlado por invariantes (`ingresar(monto)`, `retirar(monto)`), no una estructura de datos.
- **Respuesta senior:** El diseño viola el principio de encapsulamiento: saldo debe ser `private BigDecimal saldo;` (evitando errores de redondeo de punto flotante en dinero). No debe existir `setSaldo()`, sino operaciones del dominio como `ingresar(BigDecimal monto)` y `retirar(BigDecimal monto)` que validen que el monto sea positivo y que haya fondos suficientes. Para transferir datos de lectura a la vista o API, se expone un `record CuentaDto(String numero, BigDecimal saldo) { }` inmutable.


## Katas

### Kata 2.1: Record blindado contra mutaciones externas

Crea un `record Pedido(Long id, List<String> productos)` con constructor compacto que valide que id no sea nulo y que asigne `List.copyOf(productos)` para blindarlo contra modificaciones externas.

### Kata 2.2: Clase CuentaBancaria con invariantes de negocio

Escribe una clase `CuentaBancaria` con campo privado `BigDecimal saldo`. Agrega métodos `ingresar` y `retirar` que lancen `IllegalArgumentException` ante montos negativos y `SaldoInsuficienteException` si no hay saldo.


## Reglas de oro

- Nunca compares Wrappers con `==`. Usa siempre `.equals()` o primitivos.
- Los DTOs de salida y entrada en APIs deben ser `record` inmutables.
- Si un record contiene una colección, usa `List.copyOf(coleccion)` en el constructor para evitar que la modifiquen desde afuera.


## Java 17 frente a Java 21

| Característica | Java 17 | Java 21 |
| --- | --- | --- |
| Record Patterns (Desestructuración) | No disponible (preview en 19/20) | Estándar oficial en 21 LTS |
| Pattern Matching para instanceof | Estándar `if (o instanceof String s)` | Estándar |
| Uso en Spring Boot 3 | Versión mínima requerida | Totalmente soportado y recomendado |


## Proyecto guiado «Pedidos»: Modelar el catálogo con records

Representar productos, clientes y líneas de pedido como datos inmutables que no pueden nacer inválidos.

**Tareas:**

- Crea `record Producto(String sku, String nombre, BigDecimal precio)` y valida en el constructor compacto que el precio sea positivo.
- Crea `record Cliente(long id, String nombre, Email email)` con `Email` como record propio que valida el formato.
- Crea `record LineaPedido(String sku, int cantidad, BigDecimal precioUnitario)` con un método `subtotal()`.

**Está listo cuando:**

- Ningún record se puede crear con datos inválidos: los tests lo demuestran.
- No hay setters ni campos públicos mutables en el dominio.


## Fuentes oficiales

- [JEP 395: Records](https://openjdk.org/jeps/395)
- [Java 21: guía de records](https://docs.oracle.com/en/java/javase/21/language/records.html)
- [Tutorial de Java: clases y objetos](https://docs.oracle.com/javase/tutorial/java/javaOO/index.html)

