Tus clases ya modelan el dominio, protegen sus invariantes y exponen una API mínima. Y sin embargo, si imprimes un libro obtienes esto: com.nexussoftware.bibliotech.dominio.Libro@1b6d3586. Y si comparas dos ejemplares de "Java Efectivo" con el mismo ISBN, Java te dice que no son iguales. Ambos comportamientos vienen del mismo sitio: la clase Object, raíz de toda la jerarquía de Java, que proporciona implementaciones por defecto de una serie de métodos universales. Esas implementaciones funcionan, pero son deliberadamente conservadoras: toString no sabe qué es importante de tu clase y equals no sabe qué significa "igual" en tu dominio. Solo tú puedes decirlo. Esta lección te enseña a hacerlo bien, con el contrato completo de cada método, porque equals y hashCode mal escritos producen bugs que no se manifiestan hasta que tus objetos entran en una colección, y para entonces el fallo parece magia negra. Y al terminar, cerrarás el módulo cumpliendo la promesa que dejaste al final del módulo 2.

Contenido

  1. Object, la raíz de todo
  2. Recorrido por los métodos de Object
  3. toString: qué es ese @1b6d3586
  4. Sobrescribir toString correctamente
  5. equals: igualdad de referencia frente a igualdad lógica
  6. El contrato de equals
  7. Implementar equals paso a paso
  8. El error clásico: equals(Libro) en vez de equals(Object)
  9. hashCode: el contrato y por qué va siempre con equals
  10. Implementar hashCode con Objects.hash
  11. Qué se rompe si no sobrescribes hashCode
  12. getClass() frente a instanceof en equals
  13. Objects.equals y Objects.requireNonNull
  14. Cierre del módulo: la refactorización final de BiblioTech
  15. Errores Comunes y Consejos
  16. Ejercicios

  1. Object, la raíz de todo

Como viste en 03-05, toda clase Java hereda de java.lang.Object, directa o indirectamente. Si no escribes extends, el compilador lo pone por ti.

classDiagram
    Object <|-- Material
    Object <|-- Empleado
    Object <|-- Prestamo
    Object <|-- String
    Material <|-- Libro
    Material <|-- Revista
    Material <|-- Dvd
    class Object {
        +toString() String
        +equals(Object) boolean
        +hashCode() int
        +getClass() Class
        #clone() Object
    }

Esto tiene dos consecuencias prácticas de las que ya te has beneficiado sin saberlo:

  • Todo objeto tiene los métodos de Object. Puedes llamar a toString() sobre cualquier cosa.
  • Object sirve como tipo común universal. Un parámetro Object acepta cualquier objeto, y por eso System.out.println(Object) funciona con lo que le eches. Es el polimorfismo de 03-06 llevado al límite.

  1. Recorrido por los métodos de Object

Método Qué hace por defecto ¿Se sobrescribe?
toString() Devuelve NombreClase@hashHex Casi siempre
equals(Object) Compara referencias (==) Cuando la igualdad sea lógica
hashCode() Devuelve un entero derivado de la identidad Siempre que sobrescribas equals
getClass() Devuelve la clase real del objeto No (es final)
clone() Copia superficial; requiere Cloneable Desaconsejado: usa un constructor de copia (03-04)
finalize() Se invocaba antes de recolectar Obsoleto desde Java 9; nunca lo uses
wait(), notify(), notifyAll() Coordinación entre hilos No; se estudian en el módulo 8

Dos aclaraciones sobre las filas problemáticas:

clone() está desaconsejado. Su diseño obliga a implementar la interfaz marcadora Cloneable, produce copias superficiales por defecto y tiene una interacción confusa con final y la herencia. La alternativa es la que ya conoces: un constructor de copia.

finalize() no debe usarse jamás. Está marcado como obsoleto (deprecated) desde Java 9 y eliminado del uso práctico: no hay garantía de que se ejecute, ni de cuándo, ni siquiera de que se ejecute alguna vez. Para liberar recursos existe try-with-resources, que se estudia en la lección 06-06.

  1. toString: qué es ese @1b6d3586

Cuando concatenas o imprimes un objeto, Java llama a su toString():

Libro libro = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
System.out.println(libro);
com.nexussoftware.bibliotech.dominio.Libro@1b6d3586

La implementación por defecto en Object es, literalmente:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

Descompuesto:

Parte Qué es
com.nexussoftware.bibliotech.dominio.Libro El nombre completo de la clase, con paquete
@ Un separador literal
1b6d3586 El hashCode() del objeto, en hexadecimal

Y aquí conviene deshacer un malentendido muy extendido: ese número no es la dirección de memoria. Es el valor devuelto por hashCode(), que en la implementación por defecto se deriva de la identidad del objeto, pero que la JVM puede calcular de varias formas y que no cambia aunque el recolector de basura mueva el objeto de sitio. Es un identificador, no una posición.

Como identificador funciona; como información, es inútil. Por eso toString se sobrescribe casi siempre.

  1. Sobrescribir toString correctamente

Un buen toString es breve, informativo y sin efectos secundarios. Debe contener los datos que identifican al objeto para un humano.

@Override
public String toString() {
    return String.format("Libro[isbn=%s, titulo=%s, autor=%s, anio=%d, disponible=%b]",
                         getIsbn(), getTitulo(), autor, anioPublicacion, estaDisponible());
}
System.out.println(libro);
// Libro[isbn=978-0000000001, titulo=Java Efectivo, autor=Joshua Bloch, anio=2018, disponible=true]

Reglas prácticas:

Regla Motivo
Incluye los campos identificativos Es lo que buscarás en un log
Mantenlo en una línea Los logs se leen y se filtran por líneas
No lo uses como formato de datos Si alguien parsea tu toString, no podrás cambiarlo nunca
No incluyas datos sensibles Contraseñas y tokens acaban en los logs
No provoques efectos secundarios Se llama en momentos inesperados, incluso desde el depurador
Cuidado con la recursión Si Prestamo.toString() imprime el Empleado y este imprime sus préstamos, tienes un StackOverflowError

Un toString útil para Prestamo, que delega en lo que ya sabe:

@Override
public String toString() {
    return String.format("Prestamo[%s, material=%s, empleado=%s, transcurridos=%d, multa=%.2f, %s]",
                         referencia, getTituloMaterial(), getNombreEmpleado(),
                         diasTranscurridos, calcularMulta(), clasificarGravedad());
}
Prestamo[PR-0001, material=Java Efectivo, empleado=Marta Ruiz, transcurridos=20, multa=1,25, LEVE]

Ese cambio, por sí solo, mejora radicalmente la depuración: la ventana de variables del IDE y cualquier System.out.println pasan a mostrar información legible en vez de @1b6d3586.

  1. equals: igualdad de referencia frente a igualdad lógica

Retomemos el == del módulo 1, ahora con objetos propios. Hay dos nociones de igualdad:

Noción Operador Pregunta que responde
Igualdad de referencia (identidad) == ¿Son el mismo objeto en memoria?
Igualdad lógica (equivalencia) equals ¿Representan lo mismo según el dominio?

Con la implementación por defecto de Object, ambas son idénticas, porque Object.equals es exactamente esto:

public boolean equals(Object obj) {
    return (this == obj);
}

De ahí el comportamiento que sorprende:

Libro a = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
Libro b = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);

System.out.println(a == b);        // false: dos objetos distintos
System.out.println(a.equals(b));   // false: equals por defecto es ==

¿Es correcto? Depende de lo que signifique "igual" en BiblioTech. Y esa es una decisión de negocio, no técnica:

  • Si Libro representa una obra bibliográfica, dos objetos con el mismo ISBN son el mismo libro. → hay que sobrescribir equals.
  • Si Libro representa un ejemplar físico, dos ejemplares de la misma obra son cosas distintas aunque compartan ISBN. → la igualdad por identidad es correcta.

En BiblioTech adoptamos la primera interpretación: dos libros son iguales si tienen el mismo ISBN. Es la definición que usa el propio sector editorial, y encaja con nuestro vocabulario del dominio.

Recuerda además la lección del módulo 1 con String: "Java Efectivo" == otraCadena puede dar false aunque el texto sea idéntico, y por eso siempre comparabas con equals. Ahora ya sabes exactamente por qué: String sobrescribe equals para comparar el contenido.

  1. El contrato de equals

equals no es un método cualquiera: tiene un contrato que toda la biblioteca estándar de Java asume que cumples. Si lo rompes, las colecciones del módulo 5 se comportarán de forma impredecible y la culpa no será suya.

Para referencias no nulas x, y, z:

Propiedad Qué exige Ejemplo de violación
Reflexiva x.equals(x) es true Comparar un campo double que valga NaN
Simétrica Si x.equals(y) entonces y.equals(x) Un Libro que se considere igual a un String con su ISBN
Transitiva Si x.equals(y) e y.equals(z), entonces x.equals(z) Comparaciones que ignoran campos en una subclase
Consistente Llamadas repetidas devuelven lo mismo si no cambia el estado Usar un campo mutable o aleatorio
Frente a null x.equals(null) es false, nunca lanza error Olvidar la comprobación de nulidad

La violación más frecuente en código real es la simetría, y suele aparecer así:

// MAL: asimetrico
@Override
public boolean equals(Object obj) {
    if (obj instanceof String) {
        return getIsbn().equals(obj);      // un Libro "igual" a un String
    }
    // ...
}
libro.equals("978-0000000001");   // true
"978-0000000001".equals(libro);   // false   <-- roto

String.equals nunca considerará igual a un Libro, así que la relación no es simétrica y cualquier colección que dependa de ella fallará de forma aparentemente aleatoria.

Consejo derivado del contrato: compara solo con objetos de tu propio tipo.

  1. Implementar equals paso a paso

La estructura canónica tiene cinco pasos, y conviene escribirla siempre igual:

@Override
public boolean equals(Object obj) {

    // 1. Atajo por identidad: el mismo objeto siempre es igual a si mismo.
    //    Es rapido y garantiza la propiedad reflexiva.
    if (this == obj) {
        return true;
    }

    // 2. Comprobacion de tipo y de nulidad a la vez.
    //    instanceof devuelve false si obj es null, asi que cubre el contrato
    //    de "x.equals(null) es false" sin una comprobacion aparte.
    if (!(obj instanceof Libro)) {
        return false;
    }

    // 3. Conversion segura al tipo propio.
    Libro otro = (Libro) obj;

    // 4. Comparacion de los campos significativos.
    //    En BiblioTech, la identidad de un libro es su ISBN.
    return getIsbn().equals(otro.getIsbn());
}

Con el instanceof con patrón de Java 16 (lección 03-06), los pasos 2 y 3 se funden en uno:

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof Libro otro)) return false;
    return getIsbn().equals(otro.getIsbn());
}

Comprobación:

Libro a = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
Libro b = new Libro("Java Efectivo", "J. Bloch", "978-0000000001", 2017);   // datos distintos
Libro c = new Libro("Refactorización", "Martin Fowler", "978-0000000003", 2018);

System.out.println(a.equals(a));      // true   (reflexiva)
System.out.println(a.equals(b));      // true   (mismo ISBN)
System.out.println(b.equals(a));      // true   (simetrica)
System.out.println(a.equals(c));      // false  (ISBN distinto)
System.out.println(a.equals(null));   // false  (contrato con null)
System.out.println(a.equals("978-0000000001"));   // false (tipo distinto)
System.out.println(a == b);           // false  (siguen siendo dos objetos)

Fíjate en el caso a.equals(b): los títulos y años son distintos, pero el ISBN coincide, así que son el mismo libro. Eso es exactamente lo que decidimos en el apartado 5: la igualdad la define el dominio, no la coincidencia de todos los campos.

Sobre qué campos usar, tres criterios:

  1. Solo campos significativos para la identidad. Un campo derivado o de estado transitorio (como disponible) no debe entrar.
  2. Preferiblemente campos final. Si un campo usado en equals cambia, el objeto cambia de identidad, con las consecuencias del apartado 11.
  3. Cuidado con los tipos: para float y double, usa Float.compare / Double.compare en lugar de ==, porque NaN != NaN rompería la reflexividad, y 0.0 y -0.0 son == pero se distinguen en el bit.

  1. El error clásico: equals(Libro) en vez de equals(Object)

Este es el error que hay que ver una vez para no repetirlo nunca. Fíjate en la firma:

public class Libro extends Material {

    // MAL: esto NO sobrescribe Object.equals, lo SOBRECARGA
    public boolean equals(Libro otro) {
        return getIsbn().equals(otro.getIsbn());
    }
}

Compila sin ninguna queja. Y funciona... a veces:

Libro a = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
Libro b = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);

System.out.println(a.equals(b));                 // true  <-- usa TU metodo

Object oa = a;
Object ob = b;
System.out.println(oa.equals(ob));               // false <-- usa Object.equals

El mismo par de objetos, dos resultados opuestos. ¿Por qué?

Porque equals(Libro) y equals(Object) tienen firmas distintas: son una sobrecarga, no una sobrescritura (tabla de 03-05). Y la sobrecarga se resuelve en compilación, según el tipo declarado (03-06). Con a y b declarados como Libro, el compilador elige tu método; con oa y ob declarados como Object, elige el de Object, que compara referencias.

Y lo peor es que todas las colecciones del módulo 5 llaman a equals(Object), porque internamente trabajan con referencias genéricas. Así que tu método nunca se usaría donde más falta hace, y el bug aparecería como "el HashSet tiene libros duplicados" sin ninguna pista sobre la causa.

La defensa es la anotación que ya conoces:

@Override
public boolean equals(Libro otro) { ... }
error: method does not override or implement a method from a supertype

Un segundo escribiendo @Override frente a una tarde depurando. Esta es, probablemente, la razón más contundente de todas las que hemos dado para ponerla siempre.

  1. hashCode: el contrato y por qué va siempre con equals

hashCode() devuelve un entero que resume el contenido del objeto. Su uso está en las estructuras basadas en tablas hash —HashMap y HashSet, módulo 5—, que lo emplean para localizar objetos rápidamente sin comparar uno a uno.

Su contrato tiene tres cláusulas:

Cláusula Enunciado
1. Consistencia Si el objeto no cambia, hashCode() devuelve siempre el mismo valor durante la ejecución
2. Coherencia con equals Si x.equals(y) es true, entonces x.hashCode() == y.hashCode() obligatoriamente
3. Recíproca no exigida Si x.hashCode() == y.hashCode(), x e y no tienen por qué ser iguales (a eso se le llama colisión, y es normal)

La cláusula 2 es la que impone la regla de oro:

Siempre que sobrescribas equals, sobrescribe hashCode. Sin excepciones.

La lógica es inevitable: si dos objetos son "iguales" pero producen códigos hash distintos, una tabla hash los colocará en cubos distintos y nunca los comparará entre sí, por lo que jamás descubrirá que son iguales.

Buena noticia: la cláusula 3 significa que un hashCode no tiene por qué ser único. Puede haber colisiones; la estructura las resuelve comparando con equals dentro del cubo. Un hashCode que devolviera siempre 0 sería correcto (cumple el contrato) pero inútil, porque degradaría el HashMap a una búsqueda lineal.

  1. Implementar hashCode con Objects.hash

La forma moderna y recomendada es una línea, usando la clase utilitaria java.util.Objects:

import java.util.Objects;

@Override
public int hashCode() {
    return Objects.hash(getIsbn());
}

Regla imprescindible: hashCode debe usar exactamente los mismos campos que equals. Si equals compara el ISBN, hashCode se calcula con el ISBN. Si añades un campo a uno, añádelo al otro. Es la causa número uno de incoherencias.

Con varios campos:

@Override
public int hashCode() {
    return Objects.hash(referencia, diaPrestamo);
}

Objects.hash(...) es un varargs (03-03) que combina los códigos hash de sus argumentos con un algoritmo estándar. Internamente hace lo mismo que la fórmula clásica, que conviene conocer porque la verás en código antiguo y en entrevistas:

@Override
public int hashCode() {
    int resultado = 17;                                      // primo inicial
    resultado = 31 * resultado + isbn.hashCode();            // 31: primo impar
    resultado = 31 * resultado + Integer.hashCode(anio);
    return resultado;
}

El 31 se usa porque es primo impar y porque 31 * x se optimiza a (x << 5) - x. No necesitas escribirlo a mano: Objects.hash es más legible y suficientemente bueno.

Una precaución de rendimiento: Objects.hash crea un array interno para los varargs, así que en clases con millones de instancias en bucles críticos puede notarse. Para todo lo demás —incluido BiblioTech—, es la opción correcta.

  1. Qué se rompe si no sobrescribes hashCode

Este apartado justifica todo lo anterior. Supón que sobrescribes equals en Libro pero olvidas hashCode:

Libro a = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
Libro b = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);

System.out.println(a.equals(b));               // true
System.out.println(a.hashCode());              // 460141958
System.out.println(b.hashCode());              // 1163157884   <-- distintos

Son "iguales" y tienen códigos hash diferentes: la cláusula 2 está rota. Las consecuencias, que sufrirás en el módulo 5:

// Adelanto del modulo 5, solo para ver el sintoma
Set<Libro> catalogo = new HashSet<>();
catalogo.add(a);

System.out.println(catalogo.contains(b));   // false, ¡aunque a.equals(b) sea true!
catalogo.add(b);
System.out.println(catalogo.size());        // 2, con el mismo ISBN dos veces

Un HashSet que admite duplicados. Un HashMap en el que guardas con una clave y no puedes recuperar con otra idéntica. Y ningún error, ninguna excepción, ninguna pista: simplemente, resultados incorrectos.

flowchart TD
    A["catalogo.contains(b)"] --> B["Calcula b.hashCode()"]
    B --> C{"¿Hay algo
    en ese cubo?"}
    C -- "no (cubo vacio)" --> D["Devuelve false
    sin llamar nunca a equals"]
    C -- "si" --> E["Compara con equals
    dentro del cubo"]

Ahí está la clave del diagrama: si el hashCode no coincide, equals ni siquiera se llega a llamar. Por eso el bug es tan desconcertante: tu equals es perfecto y aun así el resultado es incorrecto.

Regla final, en una frase: equals y hashCode se escriben juntos, se modifican juntos y se revisan juntos.

  1. getClass() frente a instanceof en equals

Existe una decisión de diseño en el paso 2 del equals, y merece conocerse porque tiene implicaciones reales con la herencia.

Variante con instanceof (la que hemos usado):

if (!(obj instanceof Libro otro)) return false;

Variante con getClass():

if (obj == null || getClass() != obj.getClass()) return false;
Libro otro = (Libro) obj;

La diferencia aparece con subclases. Imagina una LibroFirmado extends Libro:

Comparación Con instanceof Con getClass()
libro.equals(libroFirmado) con el mismo ISBN true false
libroFirmado.equals(libro) con el mismo ISBN Depende de si la subclase sobrescribe equals false
¿Simetría garantizada? No, si la subclase añade campos a la comparación Sí, siempre
¿Permite igualdad entre clases de la jerarquía? No

El problema clásico de instanceof: si LibroFirmado sobrescribe equals añadiendo el firmante a la comparación, entonces libro.equals(libroFirmado) daría true (solo mira el ISBN) mientras que libroFirmado.equals(libro) daría false (además mira el firmante). Asimetría, y contrato roto.

Elige... Cuando...
getClass() Quieres simetría garantizada y que cada clase concreta sea su propio tipo de igualdad
instanceof Quieres que las subclases sean intercambiables, y te comprometes a que ninguna añada campos a equals
Ninguna preocupación La clase es final (entonces son equivalentes)

Decisión para BiblioTech. Como Libro puede tener subclases y queremos simetría a prueba de todo, la implementación definitiva usará getClass(). Y la alternativa más limpia sigue siendo la de 03-07: declarar final la clase cuando no esté diseñada para ser extendida, con lo que el dilema desaparece.

Nota sobre record. Un record (lección 04-07) genera automáticamente equals, hashCode y toString a partir de todos sus componentes, con el contrato correctamente implementado. Es la mejor razón para usarlos en objetos de datos inmutables. Pero fíjate en el matiz: el equals generado usa todos los componentes, mientras que el nuestro usa solo el ISBN. Si tu noción de igualdad no es "todos los campos coinciden", el record no te sirve tal cual.

  1. Objects.equals y Objects.requireNonNull

La clase java.util.Objects aporta dos utilidades que verás constantemente.

Objects.equals(a, b) compara dos referencias tratando null correctamente, sin que tengas que escribir la guarda:

// Sin Objects.equals: hay que protegerse
return (autor == null) ? (otro.autor == null) : autor.equals(otro.autor);

// Con Objects.equals: una linea, y null-safe
return Objects.equals(autor, otro.autor);

Su implementación es exactamente (a == b) || (a != null && a.equals(b)). Úsala siempre dentro de equals cuando un campo pueda ser nulo.

Objects.requireNonNull(objeto, mensaje) verifica que una referencia no sea nula y falla inmediatamente si lo es. Su valor está en fallar pronto y con un mensaje claro, en el constructor, en lugar de dejar que un null viaje por el sistema y explote tres capas más allá:

public Prestamo(Material material, Empleado empleado, int diaPrestamo, int diasTranscurridos) {
    this.material = Objects.requireNonNull(material, "El prestamo necesita un material");
    this.empleado = Objects.requireNonNull(empleado, "El prestamo necesita un empleado");
    // ...
}

Si alguien pasa null, el programa se detiene ahí mismo con:

Exception in thread "main" java.lang.NullPointerException: El prestamo necesita un material
        at com.nexussoftware.bibliotech.dominio.Prestamo.<init>(Prestamo.java:42)

Compáralo con el marcador "Material desconocido" que veníamos usando: aquel enmascaraba el error y permitía que el sistema siguiera con datos falsos. requireNonNull es la primera forma correcta de rechazar un argumento inválido que ves en el curso, y es un anticipo del módulo 6, donde aprenderás a lanzar excepciones deliberadamente. A partir de ahora, en las clases de dominio, es preferible a los avisos por consola.

  1. Cierre del módulo: la refactorización final de BiblioTech

Ha llegado el momento de cumplir la promesa que cerró el módulo 2. Recuerda el problema:

// BiblioTechApp 2.0: tres variables sueltas que describen UNA sola cosa
int    mayorRetraso         = 0;
String empleadoMayorRetraso = "-";
String libroMayorRetraso    = "-";

// ...y su actualizacion, que hay que recordar hacer entera:
if (diasRetraso > mayorRetraso) {
    mayorRetraso         = diasRetraso;
    empleadoMayorRetraso = empleado;
    libroMayorRetraso    = titulo;
}

Los tres defectos que señalamos entonces: nada garantiza que las tres se actualicen juntas; el compilador no puede ayudar; y si mañana quieres añadir la multa o la referencia, hay que añadir una cuarta variable y otra línea que recordar.

Con lo aprendido en el módulo, las tres variables se convierten en una:

// BiblioTechApp 3.0: un unico objeto coherente
Prestamo prestamoMayorRetraso = null;

// ...y su actualizacion, atomica por construccion:
if (prestamoMayorRetraso == null
        || prestamo.calcularDiasRetraso() > prestamoMayorRetraso.calcularDiasRetraso()) {
    prestamoMayorRetraso = prestamo;
}

Una sola asignación. Es imposible dejar el estado a medias, porque no hay partes que actualizar por separado: o cambia el objeto entero, o no cambia nada. Y mostrarlo es una línea, gracias a toString:

if (prestamoMayorRetraso != null) {
    System.out.println("  Mayor retraso: " + prestamoMayorRetraso);
} else {
    System.out.println("  Mayor retraso: (ninguno registrado)");
}
  Mayor retraso: Prestamo[PR-0003, material=Refactorización, empleado=Nuria Vidal, transcurridos=95, multa=20,00, GRAVE]

Compara las dos versiones:

Aspecto Módulo 2 Ahora
Variables implicadas 3 sueltas 1 objeto
Líneas en la actualización 4, todas obligatorias 1
Se puede descoordinar Imposible
Añadir la multa al informe Cuarta variable + cuarta línea Ya está dentro
Mostrarlo 3 printf 1 println
Comparar dos máximos Comparar tres variables equals

Estado final de BiblioTech tras el módulo 3

Las clases del dominio, con sus tres métodos de Object implementados:

// Libro.java (extracto: los metodos de Object)

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    Libro otro = (Libro) obj;
    return getIsbn().equals(otro.getIsbn());       // igualdad por ISBN
}

@Override
public int hashCode() {
    return Objects.hash(getIsbn());                // MISMO campo que equals
}

@Override
public String toString() {
    return String.format("Libro[isbn=%s, titulo=%s, autor=%s, anio=%d, disponible=%b]",
                         getIsbn(), getTitulo(), autor, anioPublicacion, estaDisponible());
}
// Empleado.java (extracto)

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    Empleado otro = (Empleado) obj;
    return identificador.equals(otro.identificador);   // identidad = EMP-XXX
}

@Override
public int hashCode() { return Objects.hash(identificador); }

@Override
public String toString() {
    return String.format("Empleado[%s, %s, prestamos=%d]",
                         identificador, nombre, prestamosAcumulados);
}
// Prestamo.java (extracto)

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || getClass() != obj.getClass()) return false;
    Prestamo otro = (Prestamo) obj;
    return referencia.equals(otro.referencia);         // identidad = PR-XXXX
}

@Override
public int hashCode() { return Objects.hash(referencia); }

@Override
public String toString() {
    return String.format("Prestamo[%s, material=%s, empleado=%s, transcurridos=%d, multa=%.2f, %s]",
                         referencia, getTituloMaterial(), getNombreEmpleado(),
                         diasTranscurridos, calcularMulta(), clasificarGravedad());
}

Y un main que ya no calcula nada:

package com.nexussoftware.bibliotech;

import com.nexussoftware.bibliotech.dominio.*;
import com.nexussoftware.bibliotech.presentacion.ReciboConsola;

public class BiblioTechApp {

    public static void main(String[] args) {

        ReciboConsola recibo = new ReciboConsola();

        System.out.println("=== BiblioTech 3.2 - Fin del modulo 3 ===\n");

        Libro   javaEfectivo = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
        Libro   patrones     = new Libro("Patrones de Diseño", "Erich Gamma", "978-0000000002", 1994);
        Libro   refactor     = new Libro("Refactorización", "Martin Fowler", "978-0000000003", 2018);
        Revista revista      = new Revista("Java Magazine", "REV-2024-42", 42, "Bimestral");
        Dvd     dvd          = new Dvd("Curso de Spring", "DVD-0007", 240);

        Empleado marta = new Empleado("Marta Ruiz",   "EMP-001");
        Empleado diego = new Empleado("Diego Alonso", "EMP-002");
        Empleado nuria = new Empleado("Nuria Vidal",  "EMP-003");

        // Array provisional: el modulo 5 traera las colecciones
        Prestamo[] prestamos = {
            new Prestamo(javaEfectivo, marta, 100, 20),
            new Prestamo(patrones,     diego, 100, 10),
            new Prestamo(refactor,     nuria, 100, 95),
            new Prestamo(revista,      marta, 100, 12),
            new Prestamo(dvd,          diego, 100, 12)
        };

        // Estadisticas: un objeto en lugar de tres variables sueltas
        Prestamo prestamoMayorRetraso = null;
        double   recaudacion          = 0.0;

        System.out.println("Prestamos registrados:");
        for (Prestamo p : prestamos) {
            System.out.println("  " + p);
            recaudacion += p.calcularMulta();

            if (prestamoMayorRetraso == null
                    || p.calcularDiasRetraso() > prestamoMayorRetraso.calcularDiasRetraso()) {
                prestamoMayorRetraso = p;
            }
        }

        System.out.printf("%nRecaudacion prevista: %.2f EUR%n", recaudacion);
        System.out.println("Mayor retraso: "
                + (prestamoMayorRetraso != null ? prestamoMayorRetraso : "(ninguno)"));

        // Devolucion completa del prestamo mas grave
        if (prestamoMayorRetraso != null) {
            prestamoMayorRetraso.registrarDevolucion(
                    prestamoMayorRetraso.getDiasTranscurridos());
            recibo.imprimirRecibo(prestamoMayorRetraso);
        }
    }
}

Salida:

=== BiblioTech 3.2 - Fin del modulo 3 ===

Prestamos registrados:
  Prestamo[PR-0001, material=Java Efectivo, empleado=Marta Ruiz, transcurridos=20, multa=1,25, LEVE]
  Prestamo[PR-0002, material=Patrones de Diseño, empleado=Diego Alonso, transcurridos=10, multa=0,00, SIN RETRASO]
  Prestamo[PR-0003, material=Refactorización, empleado=Nuria Vidal, transcurridos=95, multa=20,00, GRAVE]
  Prestamo[PR-0004, material=Java Magazine, empleado=Marta Ruiz, transcurridos=12, multa=0,50, LEVE]
  Prestamo[PR-0005, material=Curso de Spring, empleado=Diego Alonso, transcurridos=12, multa=4,50, GRAVE]

Recaudacion prevista: 26,25 EUR
Mayor retraso: Prestamo[PR-0003, material=Refactorización, empleado=Nuria Vidal, transcurridos=95, multa=20,00, GRAVE]

========================================
   RECIBO DE DEVOLUCION - BIBLIOTECH
  Referencia:          PR-0003
  Material:            Refactorización
  Empleado:            Nuria Vidal
  Retraso:             80 dias
  Multa:               20,00 EUR
  Gravedad:            GRAVE
========================================

Inventario del proyecto tras el módulo 3

Clase Paquete Responsabilidad
Material dominio Base de los soportes: identificación, disponibilidad, reglas de multa
Libro dominio Soporte con 15 días y 0,25 €/día; añade autor y año
Revista dominio Soporte con 7 días y 0,10 €/día; añade número y periodicidad
Dvd dominio Soporte con 3 días y 0,50 €/día; añade duración
Empleado dominio Persona autorizada; controla su límite de préstamos
Prestamo dominio Relaciona material y empleado; calcula retraso, multa y gravedad
ReciboConsola presentacion Genera los textos que ve el usuario
BiblioTechApp raíz Arranque y coordinación

Lo que sigue faltando, y ya sabes dónde se resuelve:

Carencia Módulo
Material se puede instanciar; falta obligar a implementar getTipo() 4 (clases abstractas)
No hay contratos de comportamiento reutilizables entre clases sin parentesco 4 (interfaces)
El array de préstamos es rígido: no se puede añadir ni buscar cómodamente 5 (colecciones)
La validación avisa por consola en lugar de rechazar de verdad 6 (excepciones)
Al cerrar el programa se pierde todo 7 (ficheros)

Errores Comunes y Consejos

  • Sobrescribir equals y olvidar hashCode. El error más caro de esta lección: no falla nada hasta que los objetos entran en un HashSet o un HashMap, y entonces falla en silencio.
  • Escribir equals(MiClase) en vez de equals(Object). Sobrecarga en lugar de sobrescritura. @Override lo detecta al instante.
  • Usar campos distintos en equals y hashCode. Rompe la cláusula 2 del contrato. Escríbelos siempre a la vez y con los mismos campos.
  • Usar campos mutables en equals/hashCode. Si el campo cambia mientras el objeto está en una colección, el objeto se vuelve inencontrable dentro de ella. Usa campos final.
  • Comparar double con == dentro de equals. NaN != NaN rompe la reflexividad. Usa Double.compare.
  • Formatear datos con toString para procesarlos. Si otro código parsea tu toString, se convierte en una API que no podrás cambiar. Para eso hay métodos específicos.
  • toString recursivo entre objetos que se referencian. StackOverflowError. Imprime el identificador del otro objeto, no el objeto entero.
  • Consejo: deja que el IDE los genere. IntelliJ (Alt+Insertequals() and hashCode()) y Eclipse (Alt+Shift+S) generan las tres implementaciones con el contrato correcto. Escríbelas a mano una vez para entenderlas, y después usa el generador.
  • Consejo: si la clase es un contenedor de datos inmutable, plantéate un record (04-07): te da los tres métodos gratis y con el contrato bien implementado.
  • Consejo: prueba el contrato. Cuatro assert o cuatro println comprobando reflexividad, simetría, transitividad y null cuestan un minuto y detectan el 90 % de los errores. En el módulo 11 lo harás con JUnit.

Ejercicios

Ejercicio 1: los tres métodos en Material y sus subclases

Implementa toString, equals y hashCode en Material y decide qué deben hacer las subclases:

  1. En Material, la igualdad se define por referencia (el ISBN de un libro, el código de un DVD...).
  2. Usa getClass() para garantizar la simetría, y comprueba con salida por consola que un Libro y un Dvd con la misma referencia no son iguales.
  3. Sobrescribe toString en Material incluyendo tipo, título, referencia y disponibilidad, y compruébalo con las cuatro subclases.
  4. Razona por escrito si Libro, Revista y Dvd necesitan sobrescribir equals y hashCode, o si les basta con heredarlos.

Ejercicio 2: demostrar la sobrecarga accidental

Escribe una clase de prueba que demuestre el error del apartado 8 en la misma ejecución:

  1. Una clase LibroMal con public boolean equals(LibroMal otro) (sin @Override).
  2. Dos objetos con el mismo ISBN.
  3. Compáralos con referencias declaradas como LibroMal y con referencias declaradas como Object, imprimiendo los dos resultados.
  4. Añade @Override y copia el mensaje exacto del compilador.
  5. Corrige la firma y comprueba que ahora ambos resultados coinciden.

Ejercicio 3: verificador del contrato de equals

Escribe un método static boolean verificarContrato(Object x, Object y, Object z) que compruebe las cinco propiedades del contrato de equals sobre tres objetos e imprima un informe con el resultado de cada una, más la coherencia con hashCode. Pruébalo con:

  1. Tres Libro correctos: dos con el mismo ISBN y uno distinto.
  2. Una clase LibroSinHashCode que sobrescriba equals pero no hashCode, para ver qué comprobación falla.

Soluciones

Solución 1

package com.nexussoftware.bibliotech.dominio;

import java.util.Objects;

public class Material {

    // ... campos, constructor y metodos anteriores ...

    /**
     * Dos materiales son iguales si son de la MISMA clase y comparten referencia.
     * El uso de getClass() garantiza la simetria incluso con subclases.
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Material otro = (Material) obj;
        return referencia.equals(otro.referencia);
    }

    /** Mismo campo que equals: la referencia. */
    @Override
    public int hashCode() {
        return Objects.hash(referencia);
    }

    @Override
    public String toString() {
        return String.format("%s[ref=%s, titulo=%s, disponible=%b]",
                             getTipo(), referencia, titulo, disponible);
    }
}

Comprobación de la simetría entre tipos distintos:

Libro libro = new Libro("Java Efectivo", "Joshua Bloch", "REF-001", 2018);
Dvd   dvd   = new Dvd("Curso de Spring", "REF-001", 240);   // MISMA referencia

System.out.println("libro.equals(dvd): " + libro.equals(dvd));   // false
System.out.println("dvd.equals(libro): " + dvd.equals(libro));   // false  (simetrico)
System.out.println(libro);
System.out.println(dvd);

Libro otroEjemplar = new Libro("Java Efectivo", "J. Bloch", "REF-001", 2017);
System.out.println("libro.equals(otroEjemplar): " + libro.equals(otroEjemplar));  // true
System.out.println("mismo hash: " + (libro.hashCode() == otroEjemplar.hashCode())); // true

Salida:

libro.equals(dvd): false
dvd.equals(libro): false
Libro[ref=REF-001, titulo=Java Efectivo, disponible=true]
DVD[ref=REF-001, titulo=Curso de Spring, disponible=true]
libro.equals(otroEjemplar): true
mismo hash: true

Observa un detalle elegante del toString de Material: usa getTipo(), que es polimórfico, así que cada subclase se identifica sola sin necesidad de sobrescribir toString. Es la lección 03-06 aplicada aquí.

4. ¿Deben las subclases sobrescribir equals y hashCode? No, y no deberían. Tres razones:

  • La referencia ya identifica unívocamente cualquier material del catálogo: no hay dos libros con el mismo ISBN ni dos DVD con el mismo código. Añadir autor o duracionMinutos a la comparación no distinguiría nada nuevo.
  • Sobrescribir equals en una subclase añadiendo campos es precisamente la fuente de asimetrías del apartado 12. Como Material.equals usa getClass(), un Libro y un Dvd ya nunca son iguales; el problema está resuelto en la clase base.
  • Si necesitaran comparar más campos, hashCode tendría que actualizarse a la vez en cada subclase, multiplicando las oportunidades de error.

Regla que se deduce: define la igualdad una sola vez, en el nivel más alto de la jerarquía donde tenga sentido, y usa un campo identificador estable en lugar de la suma de todos los campos.

Solución 2

package com.nexussoftware.bibliotech;

public class DemoEqualsMal {

    static class LibroMal {
        final String isbn;
        LibroMal(String isbn) { this.isbn = isbn; }

        // SOBRECARGA, no sobrescritura: la firma recibe LibroMal, no Object
        public boolean equals(LibroMal otro) {
            System.out.println("      [se ejecuta MI equals(LibroMal)]");
            return isbn.equals(otro.isbn);
        }
    }

    static class LibroBien {
        final String isbn;
        LibroBien(String isbn) { this.isbn = isbn; }

        @Override
        public boolean equals(Object obj) {
            System.out.println("      [se ejecuta MI equals(Object)]");
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            return isbn.equals(((LibroBien) obj).isbn);
        }

        @Override
        public int hashCode() { return isbn.hashCode(); }
    }

    public static void main(String[] args) {

        System.out.println("--- Version INCORRECTA ---");
        LibroMal a = new LibroMal("978-0000000001");
        LibroMal b = new LibroMal("978-0000000001");

        System.out.println("  Con tipo declarado LibroMal:");
        System.out.println("   a.equals(b) = " + a.equals(b));

        Object oa = a;
        Object ob = b;
        System.out.println("  Con tipo declarado Object:");
        System.out.println("   oa.equals(ob) = " + oa.equals(ob));

        System.out.println("\n--- Version CORRECTA ---");
        LibroBien c = new LibroBien("978-0000000001");
        LibroBien d = new LibroBien("978-0000000001");

        System.out.println("  Con tipo declarado LibroBien:");
        System.out.println("   c.equals(d) = " + c.equals(d));

        Object oc = c;
        Object od = d;
        System.out.println("  Con tipo declarado Object:");
        System.out.println("   oc.equals(od) = " + oc.equals(od));
    }
}

Salida:

--- Version INCORRECTA ---
  Con tipo declarado LibroMal:
      [se ejecuta MI equals(LibroMal)]
   a.equals(b) = true
  Con tipo declarado Object:
   oa.equals(ob) = false

--- Version CORRECTA ---
  Con tipo declarado LibroBien:
      [se ejecuta MI equals(Object)]
   c.equals(d) = true
  Con tipo declarado Object:
      [se ejecuta MI equals(Object)]
   oc.equals(od) = true

Las trazas lo dejan clarísimo: en la versión incorrecta, la llamada a través de Object ni siquiera entra en tu método —no aparece el mensaje—, porque el compilador ha resuelto la llamada a Object.equals. En la correcta, tu método se ejecuta en ambos casos.

4. Mensaje del compilador al añadir @Override:

error: method does not override or implement a method from a supertype
        @Override
        ^

Cinco palabras que te habrían ahorrado el bug entero. En un HashSet (módulo 5), la versión incorrecta produciría duplicados sin ninguna señal de error.

Solución 3

package com.nexussoftware.bibliotech;

/** Verificador didactico del contrato de equals y su coherencia con hashCode. */
public class VerificadorContrato {

    public static boolean verificarContrato(Object x, Object y, Object z) {

        System.out.println("Verificando el contrato con:");
        System.out.println("  x = " + x);
        System.out.println("  y = " + y);
        System.out.println("  z = " + z);

        boolean todoCorrecto = true;

        // 1. Reflexiva
        boolean reflexiva = x.equals(x) && y.equals(y) && z.equals(z);
        informar("Reflexiva  (x.equals(x))", reflexiva);
        todoCorrecto &= reflexiva;

        // 2. Simetrica
        boolean simetrica = (x.equals(y) == y.equals(x))
                         && (y.equals(z) == z.equals(y))
                         && (x.equals(z) == z.equals(x));
        informar("Simetrica  (x=y implica y=x)", simetrica);
        todoCorrecto &= simetrica;

        // 3. Transitiva: solo se comprueba si el antecedente se cumple
        boolean transitiva = true;
        if (x.equals(y) && y.equals(z)) {
            transitiva = x.equals(z);
        }
        informar("Transitiva (x=y e y=z implica x=z)", transitiva);
        todoCorrecto &= transitiva;

        // 4. Consistente: mismas llamadas, mismos resultados
        boolean consistente = (x.equals(y) == x.equals(y)) && (x.equals(y) == x.equals(y));
        informar("Consistente (llamadas repetidas)", consistente);
        todoCorrecto &= consistente;

        // 5. Frente a null
        boolean frenteANull = !x.equals(null) && !y.equals(null) && !z.equals(null);
        informar("Null       (x.equals(null) es false)", frenteANull);
        todoCorrecto &= frenteANull;

        // 6. Coherencia con hashCode (clausula 2 del contrato de hashCode)
        boolean coherenteHash = true;
        if (x.equals(y) && x.hashCode() != y.hashCode()) coherenteHash = false;
        if (y.equals(z) && y.hashCode() != z.hashCode()) coherenteHash = false;
        if (x.equals(z) && x.hashCode() != z.hashCode()) coherenteHash = false;
        informar("hashCode   (iguales -> mismo hash)", coherenteHash);
        todoCorrecto &= coherenteHash;

        System.out.println(todoCorrecto ? "RESULTADO: contrato CUMPLIDO\n"
                                        : "RESULTADO: contrato ROTO\n");
        return todoCorrecto;
    }

    private static void informar(String propiedad, boolean ok) {
        System.out.printf("  %-38s %s%n", propiedad, ok ? "OK" : "FALLA");
    }

    /** Clase defectuosa a proposito: equals sin hashCode. */
    static class LibroSinHashCode {
        final String isbn;
        LibroSinHashCode(String isbn) { this.isbn = isbn; }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (obj == null || getClass() != obj.getClass()) return false;
            return isbn.equals(((LibroSinHashCode) obj).isbn);
        }

        @Override
        public String toString() { return "LibroSinHashCode[" + isbn + "]"; }
        // Falta hashCode() a proposito
    }

    public static void main(String[] args) {

        System.out.println("=== CASO 1: Libro correcto ===");
        Libro x = new Libro("Java Efectivo", "Joshua Bloch", "978-0000000001", 2018);
        Libro y = new Libro("Java Efectivo", "J. Bloch",     "978-0000000001", 2017);
        Libro z = new Libro("Refactorización", "Martin Fowler", "978-0000000003", 2018);
        verificarContrato(x, y, z);

        System.out.println("=== CASO 2: equals sin hashCode ===");
        LibroSinHashCode p = new LibroSinHashCode("978-0000000001");
        LibroSinHashCode q = new LibroSinHashCode("978-0000000001");
        LibroSinHashCode r = new LibroSinHashCode("978-0000000003");
        verificarContrato(p, q, r);
    }
}

Salida:

=== CASO 1: Libro correcto ===
Verificando el contrato con:
  x = Libro[isbn=978-0000000001, titulo=Java Efectivo, autor=Joshua Bloch, anio=2018, disponible=true]
  y = Libro[isbn=978-0000000001, titulo=Java Efectivo, autor=J. Bloch, anio=2017, disponible=true]
  z = Libro[isbn=978-0000000003, titulo=Refactorización, autor=Martin Fowler, anio=2018, disponible=true]
  Reflexiva  (x.equals(x))                OK
  Simetrica  (x=y implica y=x)            OK
  Transitiva (x=y e y=z implica x=z)      OK
  Consistente (llamadas repetidas)        OK
  Null       (x.equals(null) es false)    OK
  hashCode   (iguales -> mismo hash)      OK
RESULTADO: contrato CUMPLIDO

=== CASO 2: equals sin hashCode ===
Verificando el contrato con:
  x = LibroSinHashCode[978-0000000001]
  y = LibroSinHashCode[978-0000000001]
  z = LibroSinHashCode[978-0000000003]
  Reflexiva  (x.equals(x))                OK
  Simetrica  (x=y implica y=x)            OK
  Transitiva (x=y e y=z implica x=z)      OK
  Consistente (llamadas repetidas)        OK
  Null       (x.equals(null) es false)    OK
  hashCode   (iguales -> mismo hash)      FALLA
RESULTADO: contrato ROTO

Lo revelador del caso 2: el equals es impecable —cumple las cinco propiedades— y aun así la clase está rota. Ese es exactamente el motivo de que el fallo sea tan difícil de encontrar en un proyecto real: todas las pruebas de igualdad pasan, y el sistema falla mucho más tarde, dentro de un HashMap, sin lanzar ninguna excepción.

Dos apuntes técnicos sobre el verificador. Primero, la comprobación de transitividad solo tiene sentido cuando el antecedente se cumple; si x e y no son iguales, la implicación es cierta por vacuidad y no hay nada que verificar. Segundo, este verificador es un anticipo de lo que harás en el módulo 11 con JUnit, donde estas comprobaciones se escriben como pruebas automáticas que se ejecutan en cada compilación en lugar de imprimir por consola.

Conclusión

Has cerrado el círculo. Sabes que Object es la raíz de toda la jerarquía y conoces sus métodos, incluidos los que no debes tocar (getClass), los desaconsejados (clone), los obsoletos (finalize) y los que esperan al módulo 8 (wait/notify). Entiendes qué es el críptico Libro@1b6d3586 —el nombre de la clase y el hashCode en hexadecimal, no una dirección de memoria— y sabes sustituirlo por un toString breve, informativo y sin efectos secundarios que transforma la depuración. Distingues la igualdad de referencia de la igualdad lógica, y sabes que decidir cuál aplica es una decisión del dominio: en BiblioTech, dos libros son el mismo libro si comparten ISBN. Dominas el contrato completo de equals —reflexivo, simétrico, transitivo, consistente y falso frente a null— y su implementación canónica en cinco pasos, con la variante moderna del instanceof con patrón. Has visto en ejecución el error clásico de escribir equals(Libro) en lugar de equals(Object), y por qué @Override es la diferencia entre un segundo y una tarde perdida. Conoces el contrato de hashCode, la regla inquebrantable de sobrescribirlo siempre junto a equals y con los mismos campos, Objects.hash para escribirlo en una línea, y exactamente qué se rompe si lo olvidas: un HashSet con duplicados y un equals al que nadie llega a llamar. Y añades a tu vocabulario Objects.equals y Objects.requireNonNull, la primera forma correcta de rechazar un argumento inválido que ves en el curso.

Con esto cierras el módulo 3, y BiblioTech es otro programa. Donde había veinte variables sueltas en un main de doscientas líneas, hay ahora ocho clases repartidas en tres paquetes: una jerarquía Material con tres soportes que aplican sus propias reglas por polimorfismo, un Empleado que hace cumplir su límite, un Prestamo que calcula su vencimiento al nacer y registra devoluciones completas en una sola llamada, una capa de presentación separada del dominio, y objetos que se imprimen y se comparan como es debido. Los campos son privados, las invariantes están garantizadas, la API pública está podada y añadir un soporte nuevo cuesta una clase y cero modificaciones. Y aquellas tres variables incómodas —mayorRetraso, empleadoMayorRetraso y libroMayorRetraso— son hoy un único objeto Prestamo coherente, tal como se prometió al terminar el módulo 2.

En el módulo 4, POO Avanzada, darás el salto de los mecanismos básicos a los que usan de verdad las bibliotecas profesionales de Java. Convertirás Material en una clase abstracta que no se pueda instanciar y que obligue a sus subclases a implementar lo que les toca; descubrirás las interfaces, con las que una clase puede comprometerse a varios contratos a la vez sin herencia múltiple; conocerás las clases internas y anónimas, las expresiones lambda y las referencias a métodos que cambian por completo la forma de escribir Java moderno; y terminarás con enumeraciones y records, que sustituirán esas cadenas "LEVE" y "GRAVE" por tipos seguros y generarán solos los tres métodos que acabas de escribir a mano. BiblioTech está listo para crecer.

Curso de Programación en Java

Módulo 1: Introducción a Java

Módulo 2: Flujo de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Programación Orientada a Objetos Avanzada

Módulo 5: Estructuras de Datos y Colecciones

Módulo 6: Manejo de Excepciones

Módulo 7: Entrada/Salida de Archivos

Módulo 8: Multihilo y Concurrencia

Módulo 9: Redes

Módulo 10: Temas Avanzados

Módulo 11: Frameworks y Librerías de Java

Módulo 12: Construcción de Aplicaciones del Mundo Real

© Copyright 2026. Todos los derechos reservados