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

Memoria JVM, Contrato equals/hashCode y Manejo de Excepciones

El Ciclo de Vida de los Objetos, Colisiones en Tablas Hash y Try-With-Resources en Java 17/21

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

  • 3.5h de estudio
  • 5 conceptos
  • 3 preguntas
  • 1 katas
Java 17 LTSJava 21 LTSStack vs Heapequals y hashCodeExcepcionesAutoCloseablePaso por valor
Al terminar este día sabrás 4
  • Escribir equals y hashCode correctos y explicar cómo los usa un HashMap.
  • Describir qué va al stack, qué va al heap y cómo trabaja el recolector de basura.
  • Decidir entre excepciones comprobadas y no comprobadas en un diseño moderno.
  • Cerrar recursos con try-with-resources y entender las excepciones suprimidas.

El Contrato Sagrado: equals() y hashCode()

Si dos objetos son iguales según equals(), obligatoriamente deben generar el mismo hashCode numérico. Si esto se rompe, las tablas hash no funcionarán.

En una línea Sobrescribir ambos usando Objects.equals(id, otro.id) y Objects.hash(id).

Contrato correcto y lo que pasa si lo rompes java
public final class Email {
    private final String valor;

    public Email(String valor) {
        this.valor = valor.strip().toLowerCase(Locale.ROOT);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Email otro)) return false;   // pattern matching de instanceof
        return valor.equals(otro.valor);
    }

    @Override
    public int hashCode() {
        return valor.hashCode();                          // mismos campos que equals
    }
}

Set<Email> vistos = new HashSet<>();
vistos.add(new Email("Ana@Acme.com"));
System.out.println(vistos.contains(new Email("ana@acme.com"))); // true

// Si borras hashCode(): contains devuelve false casi siempre,
// porque cada instancia tiene un hash de identidad distinto y cae en otro cubo.
Cómo funciona por dentro

equals define cuándo dos objetos representan lo mismo; hashCode devuelve un entero que las tablas hash usan para decidir en qué cubo guardar el objeto. El contrato dice: si a.equals(b) es true, entonces a.hashCode() == b.hashCode(). Lo contrario no es obligatorio: dos objetos distintos pueden compartir hash (colisión).

Así busca un HashMap: calcula hashCode() de la clave, lo mezcla y elige un cubo. Dentro del cubo recorre los elementos comparando primero el hash y después con equals. Si sobrescribes equals sin hashCode, dos objetos iguales caerán en cubos distintos y el mapa nunca encontrará el segundo. Desde Java 8, un cubo con muchas colisiones se convierte en un árbol para evitar degradarse a O(n).

equals además debe ser reflexivo, simétrico, transitivo, consistente y devolver false con null. Usa los mismos campos en ambos métodos, Objects.equals y Objects.hash para no equivocarte con los null, o deja que un record los genere.

Estructuras como HashSet y HashMap dependen de este contrato para encontrar elementos en tiempo constante O(1).

Se conecta con HashMap InternoHashSetIdentidad de Objetos

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

  1. Bloque 1: El Contrato de Identidad (0 - 25 min)

    Qué estudiar. Contrato de equals() y hashCode() en la documentación oficial de java.lang.Object.

    Pregunta a responder. ¿Por qué Objects.hash() es la forma segura de calcular el hash?

    Resultado esperado. Clase UsuarioId con implementación correcta y test JUnit demostrando que no se pierde en un HashSet.

  2. Bloque 2: Simulador de Colisiones Hash (25 - 50 min)

    Qué estudiar. Estructura interna de HashMap: buckets, listas enlazadas y árboles rojo-negro.

    Pregunta a responder. ¿Cuándo convierte Java una lista de colisiones de un cubo en un árbol binario balanceado?

    Resultado esperado. Demostración de 10 claves con el mismo hash funcionando dentro de un HashMap.

  3. Bloque 3: Manejo de Excepciones y Try-with-resources (50 - 80 min)

    Qué estudiar. Jerarquía de Throwable, AutoCloseable y excepciones suprimidas.

    Pregunta a responder. ¿Cómo implementar un recurso propio que se limpie con try-with-resources?

    Resultado esperado. Clase RecursoSeguro implements AutoCloseable probada con JUnit.

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

    Qué estudiar. Verificación de rúbrica del tutor y flash-recall de las trampas de memoria.

    Pregunta a responder. Explicar de memoria el ciclo de vida de un objeto desde que se hace new hasta que el GC lo reclama.

    Resultado esperado. Anotación final en la base de datos de estudio con los conceptos dominados.

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.

Por qué un HashMap pierde elementos si no sobrescribes hashCode()

Lo que debes entender

El mapa usa el hashCode() para calcular a qué cubo (bucket) va el objeto. Si buscas con un nuevo objeto con el mismo equals() pero distinto hashCode(), buscará en otro cubo y dirá que no existe.

Analogía para contarlo

Un archivador de carpetas clasificadas de la A a la Z. El hashCode decide en qué cajón buscar. Si buscas a "Pérez" en el cajón de la Z, nunca lo encontrarás aunque dentro del cajón supieras reconocerlo.

Lo que suele explicarse mal

Decir que equals y hashCode hacen "lo mismo". Equals compara si son iguales; hashCode calcula la dirección del cajón donde guardarlo.

Señal de que aún no lo dominas

Si dices que HashMap recorre todos los elementos de uno en uno para buscarlos con equals.

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) Si dos objetos tienen el mismo hashCode(), ¿significa obligatoriamente que son iguales según equals()? Ver respuesta

Respuesta razonada

No. El contrato solo obliga en un sentido: objetos iguales deben tener el mismo hash. Dos objetos distintos pueden compartir hash, y es inevitable porque hashCode devuelve un int (unos 4.300 millones de valores) y el número de objetos posibles es infinito. "Aa" y "BB" tienen el mismo hashCode siendo cadenas distintas.

Por eso un HashMap usa el hash para elegir cubo y después confirma con equals. Un buen hashCode reparte bien los valores para que haya pocas colisiones; uno que devuelva siempre la misma constante sería correcto, pero convertiría el mapa en una lista lenta.

Qué está evaluando la pregunta

No; se trata de una colisión hash. Dos objetos distintos pueden tener el mismo hash entero y convivir en el mismo bucket mediante una lista enlazada o árbol.

Nivel 2 (Intermedio) ¿Por qué las APIs modernas de Java y Spring Boot prefieren excepciones que hereden de RuntimeException en lugar de Exception comprobada? Ver respuesta

Respuesta razonada

Porque las excepciones comprobadas obligan a todas las capas intermedias a declararlas o capturarlas aunque no puedan hacer nada útil con ellas. Un SQLException del repositorio acabaría en la firma del servicio y del controlador, acoplando capas altas a detalles de infraestructura. Tampoco encajan con lambdas: Function<T, R> no declara throws.

En Spring la estrategia es dejar subir excepciones no comprobadas y traducirlas en un único punto (@RestControllerAdvice) a una respuesta HTTP. Spring además convierte las excepciones de acceso a datos en su jerarquía DataAccessException, no comprobada, y @Transactional hace rollback por defecto solo ante RuntimeException y Error.

Qué está evaluando la pregunta

Porque las comprobadas acoplan las firmas de métodos a detalles de infraestructura y obligan a bloques try-catch repetitivos, mientras que las unchecked pueden propagarse limpiamente hasta un @RestControllerAdvice centralizado.

Nivel 3 (Entrevista Senior) ¿Qué sucede si dentro de un bloque try-with-resources tanto el código principal como el método close() lanzan una excepción? Ver respuesta

Respuesta razonada

La excepción del cuerpo del try es la que se propaga. La de close() no se pierde: se añade a la principal como excepción suprimida y se puede consultar con getSuppressed(). Aparece en la traza como Suppressed:.

Si el cuerpo termina bien y solo falla close(), entonces se propaga la excepción de close(). Si hay varios recursos, se cierran todos en orden inverso aunque alguno falle, y cada fallo adicional se añade como suprimido.

Qué está evaluando la pregunta

La excepción del bloque principal es la que se propaga, y la excepción del método close() se adjunta como una excepción suprimida (ex.getSuppressed()), sin perder ninguna de las dos trazas de error.

Reto del tutor

Un candidato a puesto Java dice: "Para evitar escribir código de más, en mi proyecto capturo catch (Exception e) { log.error(e.getMessage()); } en todos los métodos de mis servicios". Evalúa esta afirmación como evaluador senior.

Lo que una respuesta suele tener bien

Reconoce la intención de registrar los errores para tener observabilidad.

Lo que suele quedarse en la superficie

Decir que "está mal porque es mejor capturar excepciones específicas".

El concepto de fondo que suele faltar

No entender que atrapar la excepción y no relanzarla silencia el error ("tragar la excepción"), haciendo que el llamante crea que la operación fue exitosa, y loguear solo e.getMessage() omite el stacktrace completo impidiendo depurar.

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

Es un anti-patrón crítico en Java corporativo: (1) Traga la excepción y evita que la transacción haga rollback si usa @Transactional; (2) e.getMessage() omite la causa raíz y las líneas de código del fallo; (3) Los servicios de negocio no deben capturar excepciones genéricas; deben dejar que suban o capturar excepciones específicas para envolverlas en excepciones de dominio con significado.

Proyecto «Pedidos» · paso 4 de 30

Errores de dominio e identidad

Que cada fallo de negocio tenga nombre propio y que dos pedidos se comparen por lo que son.

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. Crea excepciones no comprobadas: PedidoNoEncontradoException, TransicionInvalidaException y StockInsuficienteException.
  2. Implementa equals y hashCode de Pedido por su id y explica en un comentario por qué no por todos los campos.
  3. Añade un método que exporte un pedido a texto en un fichero usando try-with-resources.

Está listo cuando

  • Las transiciones prohibidas lanzan TransicionInvalidaException con un mensaje que dice el estado de origen y de destino.
  • Un HashSet<Pedido> no admite el mismo pedido dos veces aunque cambie de estado.

Katas: ejercicios cortos

Reglas de oro

  • Regla de oro: si sobrescribes equals(), sobrescribe SIEMPRE hashCode().
  • Todo recurso I/O (ficheros, sockets, JDBC) debe abrirse con try-with-resources.
  • No tragues excepciones vacías; relánzalas o propágalas.

Java 17 frente a Java 21

CaracterísticaJava 17 LTSJava 21 LTS
Garbage Collector por defecto G1 GC G1 GC optimizado / ZGC Generacional disponible
Se guarda automáticamente

Fuentes oficiales

Revisado el . Este día en Markdown