Contratos Fundamentales en Java

Iteradores, Ordenamiento y Contrato equals/hashCode

1 de 7
Slide 1 / 7 • Syntax Sugar Demystified

Qué hay realmente detrás de un for-each

El bucle for-each no existe en el bytecode. El compilador lo traduce a la creación de un objeto Iterator con un cursor interno que llama a hasNext() y next().

Lo que vos escribís vs Lo que compila Desazucarado
// 1. Azúcar sintáctico (Java 5+)
for (String fruta : cesta) {
    System.out.println(fruta);
}

// 2. Lo que realmente se ejecuta en Bytecode:
Iterator<String> it = cesta.iterator();
while (it.hasNext()) {
    String fruta = it.next();
    System.out.println(fruta);
}
Requisito formal: Cualquier clase puede usarse en un bucle for-each siempre que implemente la interfaz Iterable<T>, la cual exige un único método: Iterator<T> iterator().
El Cursor del Iterador en Memoria Cursor: Posición 0
[0] "Manzana"
[1] "Pera"
[2] "Uva"
[3] "Kiwi"
▲ cursor apunta aquí
it.hasNext() retorna true. Presiona it.next() para avanzar el cursor y retornar el elemento.
Slide 2 / 7 • Fail-Fast Iterators

La Pesadilla de ConcurrentModificationException

Cada colección mantiene un modCount. El iterador guarda expectedModCount. Si alterás la colección directamente mientras iterás, ambos divergen y explota la excepción.

Odómetros de Mutación (modCount) Sincronizados (5 == 5)
lista.modCount 5 Mutaciones en la lista
==
it.expectedModCount 5 Copia que guarda el iterador
Estado normal: modCount coincide con expectedModCount. Todo llamado a next() es seguro.

¿Por qué falla de inmediato (Fail-Fast)?

// Implementación real de ArrayList.Itr:
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

// La forma correcta de borrar mientras iterás:
Iterator<String> it = lista.iterator();
while (it.hasNext()) {
    String item = it.next();
    if (item.startsWith("X")) {
        it.remove(); // Actualiza ambos contadores
    }
}
No requiere múltiples hilos: A pesar de llamarse "Concurrent", esta excepción ocurre con frecuencia en un solo hilo cuando intentás borrar de una lista dentro de un `for-each`.
Slide 3 / 7 • Intrinsic Ordering

Comparable: El Orden Natural de una Clase

Comparable<T> impone un único orden intrínseco con compareTo(T otro). El signo del resultado gobierna la posición relativa.

Balanza de compareTo(otro): A vs B Resultado: +2 (A > B)
8 - 6 = +2 > 0
this.nota (8) - otro.nota (6) = +2. Número positivo indica que Estudiante A va DESPUÉS que B.

La Regla del Signo en compareTo

Negativo (< 0) this < otro (va antes)
Cero (== 0) this == otro (empate)
Positivo (> 0) this > otro (va después)
public class Estudiante implements Comparable<Estudiante> {
    private int nota;

    @Override
    public int compareTo(Estudiante otro) {
        return Integer.compare(this.nota, otro.nota);
    }
}
Slide 4 / 7 • Flexible Strategies

Comparator: Múltiples Criterios de Ordenamiento a Demanda

Cuando necesitás ordenar por diferentes criterios (edad, nombre, salario) sin modificar la clase original o combinar ordenamientos.

Lista de Empleados: Reordenamiento Dinámico Orden: Por Edad

Encadenamiento con Lambdas en Java 8+

// Orden por salario descendente, luego por apellido
Comparator<Empleado> comp = Comparator
    .comparing(Empleado::getSalario)
    .reversed()
    .thenComparing(Empleado::getApellido);

// Aplicar a lista:
empleados.sort(comp);

// O en Stream:
empleados.stream()
    .sorted(comp)
    .forEach(System.out::println);
Comparable vs Comparator: Comparable define el orden por defecto dentro de la clase. Comparator define estrategias externas fuera de la clase sin tocar su código.
Slide 5 / 7 • Object Identity

El Contrato equals(): Las 5 Reglas Inviolables

Reflexiva, simétrica, transitiva, consistente y segura ante null. El peligro oculto de romper la simetría con instanceof en herencia.

Los 5 Mandamientos de equals(Object o) Java Language Spec
  • 1
    Reflexiva: x.equals(x) debe retornar true siempre.
  • 2
    Simétrica: x.equals(y) debe retornar lo mismo que y.equals(x).
  • 3
    Transitiva: Si x.equals(y) y y.equals(z), entonces x.equals(z).
  • 4
    Consistente: Múltiples llamadas retornan lo mismo salvo que se mute el objeto.
  • 5
    Null-check: x.equals(null) debe retornar false (nunca lanzar NullPointerException).

La Trampa Mortal de `instanceof` en Subclases

// Punto(x, y) usa instanceof
Punto p = new Punto(1, 2);
PuntoColor pc = new PuntoColor(1, 2, "Rojo");

// ¡Rompe la simetría!
p.equals(pc);  // true (pc es instanceof Punto)
pc.equals(p);  // false (p no tiene color)

// Solución canónica:
if (this.getClass() != obj.getClass()) 
    return false;
Regla arquitectónica: No hay forma perfecta de extender una clase instanciable y agregarle un nuevo componente de valor preservando la simetría y transitividad de `equals` si se usa `instanceof`. Preferí composición sobre herencia.
Slide 6 / 7 • The Golden Rule

El Contrato de Oro: equals y hashCode

Si dos objetos son iguales según equals, deben tener idéntico hashCode. Si rompés esta regla, tus objetos desaparecen dentro de un HashSet o HashMap.

Búsqueda en HashSet: set.contains(new Producto("A1", 50)) Modo: Con Violación de Hash
Bucket [0]
vacio
Bucket [1]
Producto("A1") addr: 0x9AF1
Bucket [2]
vacio
Bucket [3]
Sobrescribiste equals() pero NO hashCode(). El objeto insertado usó su dirección de memoria (Bucket 1).

El Contrato Formal de Java

1. Si dos objetos son iguales por equals():
a.equals(b) == true ⇒ a.hashCode() == b.hashCode() (MANDATORIO).
2. Si dos objetos tienen diferente hashCode():
Son distintos con total certeza. El Set ni siquiera revisa el bucket.
3. Si tienen el mismo hashCode() pero son distintos:
Es una colisión normal. Se resuelve comparando con equals() dentro del bucket.
Slide 7 / 7 • Silent Memory Leaks

Claves Mutables en Colecciones Hash: El Objeto Atrapado

Modificar los campos de un objeto que ya está dentro de un Set o Map altera su hash futuro pero no mueve su cubeta, dejándolo inalcanzable para siempre.

El Objeto Fantasma en el Bucket Fuga de Memoria
1. Inserción Inicial:

p = new Persona("Carlos"); set.add(p);
"Carlos".hashCode() apunta al Bucket [2]. El objeto descansa ahí.

2. Mutación Prohibida:

p.setNombre("Alberto");
El objeto muta en el Heap. Ahora su nuevo hash apunta al Bucket [6], pero el objeto sigue físicamente atrapado en la cubeta 2.

3. Búsqueda y Borrado Rotos:

set.contains(p) calcula el hash de "Alberto", busca en Bucket 6 (¡está vacío!). Retorna false. Tampoco podés borrarlo con set.remove(p).

Presiona simular para observar cómo mutar una propiedad rompe la colección hash.

Regla de Diseño Inviolable

Claves Inmutables Siempre:
Toda clase utilizada como clave en un Map o elemento en un Set debe ser inmutable (o al menos los campos que participan en hashCode() y equals() deben ser final).
// Diseño seguro: Java 14+ Record
public record UsuarioId(String codigo, int sucursal) {
    // Inmutable por diseño: 
    // equals() y hashCode() generados de forma 
    // coherente y determinística.
}