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
Object, la raíz de todo- Recorrido por los métodos de
Object toString: qué es ese@1b6d3586- Sobrescribir
toStringcorrectamente equals: igualdad de referencia frente a igualdad lógica- El contrato de
equals - Implementar
equalspaso a paso - El error clásico:
equals(Libro)en vez deequals(Object) hashCode: el contrato y por qué va siempre conequals- Implementar
hashCodeconObjects.hash - Qué se rompe si no sobrescribes
hashCode getClass()frente ainstanceofenequalsObjects.equalsyObjects.requireNonNull- Cierre del módulo: la refactorización final de BiblioTech
- Errores Comunes y Consejos
- Ejercicios
Object, la raíz de todo
Object, la raíz de todoComo 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 atoString()sobre cualquier cosa. Objectsirve como tipo común universal. Un parámetroObjectacepta cualquier objeto, y por esoSystem.out.println(Object)funciona con lo que le eches. Es el polimorfismo de 03-06 llevado al límite.
- Recorrido por los métodos de
Object
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.
toString: qué es ese @1b6d3586
toString: qué es ese @1b6d3586Cuando 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);La implementación por defecto en Object es, literalmente:
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.
- Sobrescribir
toString correctamente
toString correctamenteUn 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());
}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.
equals: igualdad de referencia frente a igualdad lógica
equals: igualdad de referencia frente a igualdad lógicaRetomemos 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:
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
Librorepresenta una obra bibliográfica, dos objetos con el mismo ISBN son el mismo libro. → hay que sobrescribirequals. - Si
Librorepresenta 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 sí sobrescribe equals para comparar el contenido.
- El contrato de
equals
equalsequals 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
}
// ...
}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.
- Implementar
equals paso a paso
equals paso a pasoLa 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:
- Solo campos significativos para la identidad. Un campo derivado o de estado transitorio (como
disponible) no debe entrar. - Preferiblemente campos
final. Si un campo usado enequalscambia, el objeto cambia de identidad, con las consecuencias del apartado 11. - Cuidado con los tipos: para
floatydouble, usaFloat.compare/Double.compareen lugar de==, porqueNaN != NaNrompería la reflexividad, y0.0y-0.0son==pero se distinguen en el bit.
- El error clásico:
equals(Libro) en vez de equals(Object)
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.equalsEl 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:
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.
hashCode: el contrato y por qué va siempre con equals
hashCode: el contrato y por qué va siempre con equalshashCode() 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, sobrescribehashCode. 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.
- Implementar
hashCode con Objects.hash
hashCode con Objects.hashLa forma moderna y recomendada es una línea, usando la clase utilitaria java.util.Objects:
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:
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.
- Qué se rompe si no sobrescribes
hashCode
hashCodeEste 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 <-- distintosSon "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 vecesUn 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.
getClass() frente a instanceof en equals
getClass() frente a instanceof en equalsExiste 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):
Variante con getClass():
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? | Sí | 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. Unrecord(lección 04-07) genera automáticamenteequals,hashCodeytoStringa 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: elequalsgenerado usa todos los componentes, mientras que el nuestro usa solo el ISBN. Si tu noción de igualdad no es "todos los campos coinciden", elrecordno te sirve tal cual.
Objects.equals y Objects.requireNonNull
Objects.equals y Objects.requireNonNullLa 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.
- 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 | Sí | 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
equalsy olvidarhashCode. El error más caro de esta lección: no falla nada hasta que los objetos entran en unHashSeto unHashMap, y entonces falla en silencio. - Escribir
equals(MiClase)en vez deequals(Object). Sobrecarga en lugar de sobrescritura.@Overridelo detecta al instante. - Usar campos distintos en
equalsyhashCode. 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 camposfinal. - Comparar
doublecon==dentro deequals.NaN != NaNrompe la reflexividad. UsaDouble.compare. - Formatear datos con
toStringpara procesarlos. Si otro código parsea tutoString, se convierte en una API que no podrás cambiar. Para eso hay métodos específicos. toStringrecursivo 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+Insert→ equals() 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
asserto cuatroprintlncomprobando reflexividad, simetría, transitividad ynullcuestan 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:
- En
Material, la igualdad se define porreferencia(el ISBN de un libro, el código de un DVD...). - Usa
getClass()para garantizar la simetría, y comprueba con salida por consola que unLibroy unDvdcon la misma referencia no son iguales. - Sobrescribe
toStringenMaterialincluyendo tipo, título, referencia y disponibilidad, y compruébalo con las cuatro subclases. - Razona por escrito si
Libro,RevistayDvdnecesitan sobrescribirequalsyhashCode, 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:
- Una clase
LibroMalconpublic boolean equals(LibroMal otro)(sin@Override). - Dos objetos con el mismo ISBN.
- Compáralos con referencias declaradas como
LibroMaly con referencias declaradas comoObject, imprimiendo los dos resultados. - Añade
@Overridey copia el mensaje exacto del compilador. - 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:
- Tres
Librocorrectos: dos con el mismo ISBN y uno distinto. - Una clase
LibroSinHashCodeque sobrescribaequalspero nohashCode, 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())); // trueSalida:
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
autoroduracionMinutosa la comparación no distinguiría nada nuevo. - Sobrescribir
equalsen una subclase añadiendo campos es precisamente la fuente de asimetrías del apartado 12. ComoMaterial.equalsusagetClass(), unLibroy unDvdya nunca son iguales; el problema está resuelto en la clase base. - Si necesitaran comparar más campos,
hashCodetendrí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) = trueLas 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:
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
- Introducción a Java
- Configuración del Entorno de Desarrollo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Operadores
- Entrada y Salida por Consola
- Tu Primer Programa Completo: BiblioTech
Módulo 2: Flujo de Control
- Sentencias Condicionales
- Bucles
- Sentencias Switch
- Break y Continue
- Depuración y Trazas de Ejecución
- Proyecto: Menú Interactivo de BiblioTech
Módulo 3: Programación Orientada a Objetos
- Introducción a la POO
- Clases y Objetos
- Métodos
- Constructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- La Clase Object: equals, hashCode y toString
Módulo 4: Programación Orientada a Objetos Avanzada
- Interfaces
- Clases Abstractas
- Clases Internas
- Clases Anónimas
- Expresiones Lambda
- Interfaces Funcionales y Referencias a Métodos
- Enumeraciones y Registros
Módulo 5: Estructuras de Datos y Colecciones
- Arreglos
- El Framework de Colecciones
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cola y Deque
- Pila
- Ordenación y Búsqueda en Colecciones
Módulo 6: Manejo de Excepciones
- Introducción a las Excepciones
- Bloque Try-Catch
- Throw y Throws
- Excepciones Personalizadas
- Bloque Finally
- Try-with-resources y AutoCloseable
- Estrategias de Manejo de Errores y Logging
Módulo 7: Entrada/Salida de Archivos
- Lectura de Archivos
- Escritura de Archivos
- Flujos de Archivos
- BufferedReader y BufferedWriter
- Serialización
- La API NIO.2: Path y Files
- Formatos de Intercambio: CSV y Properties
Módulo 8: Multihilo y Concurrencia
- Introducción al Multihilo
- Creación de Hilos
- Ciclo de Vida de un Hilo
- Sincronización
- Utilidades de Concurrencia
- Colecciones Concurrentes y Variables Atómicas
- Tareas Asíncronas con CompletableFuture
Módulo 9: Redes
- Introducción a las Redes
- Sockets
- ServerSocket
- DatagramSocket y DatagramPacket
- URL y HttpURLConnection
- El Cliente HTTP Moderno
Módulo 10: Temas Avanzados
- Genéricos
- Anotaciones
- Reflexión
- Características de Java 8: Streams y Optional
- Fechas y Horas con java.time
- Java 9 y Más Allá
- Memoria, Recolección de Basura y Rendimiento
Módulo 11: Frameworks y Librerías de Java
- Introducción a los Frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Pruebas Avanzadas con Mockito
- Librerías Esenciales del Ecosistema
