En esta sección efiniremos brevemente cómo analizar la igualdad entre dos variables. Este concepto es clave ya que se utiliza de forma regular para comparar, por ejemplo, si existe cierto elemento en un arreglo.
Un tipo de igualdad de identidad o de referencia aplica para determinar si dos variables tienen la misma referencia a una instancia. Por otro lado, si queremos determinar la igualdad semántica o lógica deberíamos implementarla contemplando tanto los tipos de datos como los estados de los objetos comparados.
La igualdad de identidad podemos realizarla con el operador ==, mientras que la semántica se puede hacer a través del método equals que está definido en Object. Por defecto, el método equals realiza una comparación de identidad, tal como está implementado en Object. Entonces, si deseamos definir una igualdad más apropiada para nuestras clases, debemos sobreescribir el método equals.
Veamos el siguiente ejemplo:
Integer a = 10;
Integer b = Integer.valueOf(10);
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
Ambas variables de tipo Integer pueden compararse con el operador == para determinar si son la misma referencia, caso contrario el compilador detectaría que son de distinto tipo y no podrían compararse así. En este caso, la comparación de identidad es falsa porque b es una nueva instancia explícita de Integer. La comparación de igualdad sí es verdadera porque en ese caso el método equals de Integer (sboreescribe al de Object) compara el valor entero que almacena esa referencia.
En el caso de variables primitivas, el operador == realiza una comparación de igualdad, ya que no son variables de referencia y no pueden compararse en su identidad.
Recordemos las propiedades algebraicas que deben cumplirse en una relación de equivalencia en la teoría de conjuntos, pensándolas desde la igualdad:
Estas propiedades son importantes al momento de definir la igualdad en nuestras clases cuando implementamos nuestra versión del método equals.
Según la API de Java8: https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#equals-java.lang.Object-, podemos ver que se apoya en las propiedades previas.
The equals method implements an equivalence relation on non-null object references:
- It is reflexive: for any non-null reference value x, x.equals(x) should return true.
- It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
- It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
- It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
- For any non-null reference value x, x.equals(null) should return false.
Le agrega la idea de consistencia para reforzar que la comparación devuelva el mismo valor siempre y cuando el estado de los objetos comparados no cambie. También agrega el detalle de comparar contra null.
Lectura de interés: Item 10: Obey the general contract when overriding equals - Effective Java 3rd, de Joshua Bloch.
La firma del método equals definido en Object es la siguiente:
@Override
public boolean equals(Object otro)
Podemos seguir los siguientes pasos para implementar (sobreescribir) nuestro método equals.
Verificar misma referencia
Siempre validamos la propiedad de reflexividad, si es la misma referencia será también igual con el equals.
if (this == otro) {
return true;
}
Verificar null
Cuando comparamos contra un null, devolvemos falso (última condición del contrato).
if (otro == null) {
return false;
}
Verificar misma clase o superclase
Este punto puede ser controversial, porque depende de la abstracción que estemos modelando. Podríamos comparar si el otro objeto es del mismo tipo exacto (misma clase) así:
if (this.getClass() != otro.getClass()) {
return false;
}
Si bien es válido para comparar objetos de misma clase, puede fallar para comparar objetos de subclase y superclase. Por ejemplo, si comparamos una Persona con Estudiante devolvería siempre falso más allá que tengan mismo nombre. Otra opción sería:
if (!(otro instanceof Persona)) {
return false;
}
El operador instanceof valida si una instancia es de cierto tipo, lo cual incluye comparar el tipo exacto y todos los supertipos (recordemos que por herencia un Estudiante es de tipo Estudiante y Persona). Así es posible comparar objetos de distintas clases en la jerarquía de herencia, pero también se podría violar la propiedad de simetría. Si Estudiante sobreescribe el equals para comparar por un campo nuevo (la matrícula), comparar Persona con Estudiante llamaría al equals de Persona, mientras que Estudiante con Persona llamaría al nuevo equals que valida este nuevo campo que no existe en Persona.
Persona objPersona = new Persona("Juana"); // Nombre
Estuudiante objEstudiante = new Estudiante("Juana", 12345678); // Nombre y matrícula
objPersona.equals(objEstudiante); // Invoca el equals de Persona
objEstudiante.equals(objPersona); // Invoca el equals de Estudiante
Una alernativa sería distinguir en el equals de Estudiante si estamos comparando contra una instancia de Estudiante o Persona para evitar comparar la matrícula si otro es de tipo Persona. El problema más sutil con esta opción es que viola la propiedad de transitividad, porque podría suceder lo siguiente:
Persona objPersona = new Persona("Juana");
Estuudiante objEstudiante1 = new Estudiante("Juana", 12345678);
Estuudiante objEstudiante2 = new Estudiante("Juana", 87654321);
objEstudiante1.equals(objPersona); // true: mismo nombre
objPersona.equals(objEstudiante2); // true: mismo nombre
objEstudiante1.equals(objEstudiante2); // false: distinta matrícula, cuando por transitividad debería ser true
En conclusión, dependerá de la situación que modelemos sobre qué alternativa de validación de clase utilizar. Deberemos recordar:
Verificar atributos
El último paso consiste en la validación de los campos relevantes que deseamos incluir para verificar la igualdad de dos instancias. Por ejemplo, el nombre o número de documento de una persona.
// Asumiendo que documento es tipo int y nombre es String
return this.getDocumento() == otro.getDocumento()
&& Objects.equals(this.getNombre(), otro.getNombre());
Resumiendo, un ejemplo de implementación para MiClase podría ser:
@Override
public boolean equals(Object otro) {
if (this == otro) {
return true;
}
if (otro == null || this.getClass() != otro.getClass()) {
return false;
}
MiClase otro2 = (MiClase) otro; // Downcasting a MiClase
return this.getAtrPrimitivo() == otro2.getAtrPrimitivo()
&& Objects.equals(this.getAtrReferencia(), otro2.getAtrReferencia());
}
Lectura de interés: Item 11: Always override hashCode when you override equals - Effective Java 3rd, de Joshua Bloch.
La sobreescritura del método equals es necesaria para definir la igualdad de dos objetos, pero no es suficiente. Internamente Java utiliza en ciertos casos una función hash para mejorar la performance en la comparación, por ejemplo en estructuras de datos como HashMap. Básicamente, el método hashCode (también definido en Object) provee un valor entero (hash) que representa el estado de una instancia.
objeto.hashCode() -> NÚMERO ENTERO
Como toda función hash, existe el riesgo de colisión porque el rango de valores posibles del valor entero no puede representar a todos los objetos del sistema posibles con un único hash. Por lo tanto, pueden existir objetos distintos que compartan un mismo hash (no es lo ideal), pero siempre debemos garantizar que dos objetos iguales tengan el mismo hash.
Similar al método equals, Java establece ciertas reglas que deben cumplirse con el hashCode: https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#hashCode–
- Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
- If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
- It is not required that if two objects are unequal according to the equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.
Entonces siempre que sobreescribimos equals debemos también sobreescribir hashCode, que debe devolver el mismo valor si dos objetos son iguales e intentar que, si son distintos, el valor sea diferente (no entraremos en detalle con esto por ahora). Una forma simple sin entrar en detalles de performance sería apoyarnos en otra utilidad del paquete java.util: Objects.hash.
@Override
public int hashCode() {
return Objects.hash(atributo1, atributo2, atributo3);
}
Los atributos que se deben incluir son los que se comparan para determinar la igualdad o un subconjunto de ellos. La razón de usar un subconjunto es para evitar problemas de rendimiento y de inconsistencias cuando se mutan objetos dentro de una colección que utiliza hashCode para indexar sus elementos. A modo práctico, por el momento es recomendable incluir aquellos atributos que se usan en el equals que no sean mutables.
A continuación se muestra un algoritmo de hashCode tal como se implementa en la clase Arrays, utilizando el número primo 31 para mejorar la distribución de valores y reducir la posibilidad de colisiones.
public static int hashCode(Object[] a) {
if (a == null)
return 0;
int result = 1;
for (Object element : a)
result = 31 * result + (element == null ? 0 : element.hashCode());
return result;
}
Esta misma implementación podría utilizarse para nuestras clases donde, en lugar de tener un arreglo de Object, usaríamos un conjunto de atributos no mutables que son parte de la validación del equals.
Mencionamos que para definir el hashCode de una clase es recomendable utilizar atributos inmutables. Esta recomendación busca evitar problemas en estructuras de datos donde se almacenan los elementos indexados a través de su hashCode. Un ejemplo es el caso de Hashmap donde se generan nodos que tienen un atributo hash (inmutable) donde se coloca el hashCode de la key (lo veremos más adelante en Colecciones). Entonces, si cambiásemos el valor de atributos de un objeto que impactan en un nuevo hashCode, entonces el índice de la estructura quedaría inconsistente, porque ese cambio no produce automáticamente una actualización de las estructuras que contienen esos objetos (no se generan nuevos nodos con el nuevo hashcode). Por ese motivo, lo ideal sería que el hashCode de un objeto nunca cambie durante su tiempo de vida, es decir, se compute a partir de atributos inmutables.
El concepto de inmutabilidad está fuertemente relacionado a la programación funcional, donde se trabaja evaluando funciones puras que no tienen efectos secundarios, en lugar de utilizar de objetos y estados en memoria cambiantes. Es otro paradigma de programación, uno que no abordaremos en este curso, pero sin dudas muy interesante para incorporar eventualmente.
La inmutabilidad se refiere a la incapacidad de un objeto para cambiar su estado después de su creación. En Java, esto implica que una vez que se ha creado un objeto inmutable, sus atributos internos no pueden ser modificados. Por lo tanto, no puede cambiar su estado.
Diseñar soluciones con elementos inmutables nos provee algunos beneficios.
La inmutabilidad simplifica la lógica del programa al reducir la cantidad de cambios de estado posibles. Esto hace que el código sea más fácil de entender ya que no es necesario rastrear cambios en el estado a lo largo del tiempo.
Cuando se crea una instancia de una clase inmutable, sus valores no pueden ser modificados. Esto ayuda a prevenir cambios accidentales en el estado del objeto, lo que puede conducir a resultados inesperados o errores difíciles de rastrear.
En entornos con concurrencia (multi-threading), las clases inmutables eliminan la necesidad de sincronización para evitar problemas de concurrencia. Dado que no hay posibilidad de cambios en el estado, varios hilos (threads) pueden acceder y utilizar objetos inmutables de manera segura sin preocuparse por conflictos o inconsistencias.
Al diseñar clases inmutables, se facilita la adopción de principios funcionales, como la creación de funciones puras y la composición de operaciones.
En ciertos casos, los compiladores y entornos de ejecución pueden optimizar el código que involucra objetos inmutables, ya que la falta de cambios de estado permite realizar ciertas optimizaciones.
Veamos cómo podemos diseñar clases inmutables en Java. Recordemos que necesitamos garantizar que sus atributos no cambien luego de inicializarse. Para lograrlo, debemos contemplar las siguientes recomendaciones.
finalDeclarar la clase con el operador final nos asegura que no pueda ser extendida por otras clases, evitando posibles especializaciones que podrían comprometer la inmutabilidad.
public final class MiClaseInmutable {
// ...
}
private y finalMarcar los atributos de la clase como private y final para garantizar que no puedan ser modificados una vez que se haya inicializado la instancia de la clase.
public final class MiClaseCasiInmutable {
private final int numero;
private final String[] cadenas;
public MiClaseCasiInmutable(int numero, String[] cadenas) {
this.numero = numero;
this.cadenas = cadenas;
}
}
En este punto, debemos prestar atención a qué tipo de atributo es, ya que si es primitivo realmente será inmutable su valor, pero si es de tipo referencia sólo garantizamos que la referencia es inmutable.
En nuestro ejemplo, el atributo numerose inicializa con el constructor con el valor pasado en el primer parámetro, y lo mismo ocurre con el atributo cadenas. La diferencia es que cadenas no es primitivo y el valor que almacena es la referencia al arreglo de tipo String[] pasado como segundo parámetro. Al declarar cadenas como final sólo garantizamos que esa referencia no cambie luego de inicializarlo, pero sí podremos modificar el objeto al cual apunta (modificando elementos del arreglo).
Una forma de mejorar el diseño sería encapsulando una copia propia de ese atributo de tipo referencia para evitar que alguien lo pueda modificar por fuera. Ojo que también deberíamos cuidarnos de no ofrecer un getter de ese atributo directo, es decir, no exponer ese objeto original de nuestra estructura (porque no es realmente inmutable y podrían modificarlo).
public final class MiClaseCasiInmutable2 {
private final int numero;
private final String[] cadenas;
public MiClaseCasiInmutable2(int numero, String[] cadenas) {
this.numero = numero;
this.cadenas = Arrays.copyOf(cadenas, cadenas.length);
}
// No definir un getter que devuelva la referencia del atributo cadenas
}
Si bien esta versión es un poco mejor que la previa, debemos tener cuidado que no se pueda modificar el objeto que apunta el atributo cadenas. Por eso suele ser ideal que el tipo de dato del atributo sea también inmutable para no tener que estar pendiente de esos cuidados.
Las colecciones incorporadas en Java ofrecen métodos de construcción de estructuras inmutables, por ejemplo
Collections.unmodifiableListpara generar una lista inmutable.
Eliminar cualquier método que permita modificar los atributos después de la creación de la instancia. Esto incluye evitar métodos setters y proporcionar solo métodos de acceso, preferentemente sin exponer las referencias originales. Para esto último se podrían generar copias de los objetos de atributos internos cuando se los consumen con los getters.
public final class MiClaseInmutable {
private final int numero;
private final String[] cadenas;
public MiClaseInmutable(int numero, String[] cadenas) {
this.numero = numero;
this.cadenas = Arrays.copyOf(cadenas, cadenas.length);
}
// Solo métodos de acceso, sin setters
public int getNumero() {
return numero;
}
public String[] getCadenas() {
return Arrays.copyOf(cadenas, cadenas.length);
}
}
Si necesitamos generar una versión modificada de nuestro objeto inmutable, podemos definir una operación, estilo factory method, que devuelva un nuevo objeto en lugar de modificar el estado del actual.
public final class MiClaseInmutable {
private final int numero;
private final String[] cadenas;
public MiClaseInmutable(int numero, String[] cadenas) {
this.numero = numero;
this.cadenas = Arrays.copyOf(cadenas, cadenas.length);
}
public MiClaseInmutable duplicarNumero() {
// Crea y devuelve un nuevo objeto en lugar de modificar el estado actual
return new MiClaseInmutable(numero * 2, cadenas);
}
}
Según Item 13: Override clone judiciously - Effective Java 3rd, de Joshua Bloch, evitar implementar el clonado de objetos inmutables, ya que carece de sentido al no poder cambiar su estado.
Diseñar una clase inmutable llamada Persona que tenga como atributos nombre, apellido y documento. El documento debe ser de tipo Documento, una clase también inmutable, y se modela con un número entero, una fecha de emisión y otra fecha de vencimiento.
Persona con un único constructor que inicialice todos sus atributos.Documento con un único constructor que acepta el número del documento, luego la fecha de emisión será la fecha actual del sistema y su vencimiento será la actual + 10 años.Finalmente veamos un ejemplo de cómo se comparan los objetos utilizando el método equals o el operador ==.
public class Main {
public static void main(String[] args) {
MiClase objeto1 = new MiClase("A", 1);
MiClase objeto2 = new MiClase("A", 1);
// Comparación usando equals
boolean sonIguales = objeto1.equals(objeto2);
System.out.println("¿Son iguales? " + sonIguales);
// Comparación de identidad
boolean sonMismaReferencia = (objeto1 == objeto2);
System.out.println("¿Son el mismo objeto? " + sonMismaReferencia);
}
}
En tu sistema de gestión universitaria tenés estudiantes de grado y posgrado. Necesitamos crear clases EstudianteGrado y EstudiantePosgrado que hereden de una clase base llamada Estudiante para manejar tanto a los estudiantes de grado como a los de posgrado.
a) Crear una clase base llamada Estudiante con los siguientes atributos y métodos:
b) Crear una clase EstudianteGrado que herede de Estudiante y agregue los siguientes atributos:
c) Crear una clase EstudiantePosgrado que herede de Estudiante y agregue los siguientes atributos:
d) Sobrescribir el método equals en las clases Estudiante, EstudianteGrado y EstudiantePosgrado para comparar dos estudiantes en función de su matrícula y su carrera (ya sea grado o posgrado).
e) Crear un programa de prueba (Main) que:
Así como determinar la igualdad de dos objetos es un concepto clave, también puede serlo compararlos a través de cierta relación de orden. En casos donde sea necesario definir esta relación, podemos utilizar la interfaz que trae incorporada Java Comparable que nos permitirá aprovechar los algoritmos de ordenamiento predefinidos en las Colecciones de Java.
public interface Comparable<T> {
public int compareTo(T o);
}
La interfaz Comparable de Java es un tipo genérico y contiene un único método compareTo que debemos sobreescribir cuando la implementamos. Este método compara al objeto con uno recibido por parámetro y retorna:
Si el objeto recibido por parámetro no puede compararse con el objeto actual, el método lanza la excepción ClassCastException.
Al momento de implementar el método compareTo debemos respetar las siguientes restricciones descriptas en la documentación de Java:
sgn(x.compareTo(y)) == -sgn(y.compareTo(x)) para todos los x e y.x.compareTo(y)>0 y y.compareTo(z)>0 implica que x.compareTo(z)>0.x.compareTo(y)==0 implica que sgn(x.compareTo(z)) == sgn(y.compareTo(z)), para todos los z.(x.compareTo(y)==0) == x.equals(y)La operación sgn es la función matemática signum que devuelve -1, 0 o 1 según si el parámetro es negativo, 0 o positivo.
Por ejemplo, si quisiéramos definir el orden en las personas a través del número de documento, podríamos hacer lo siguiente.
public class Persona implements Comparable<Persona> {
// Atributos y métodos...
public int compareTo(Persona otro) {
// Asumiendo que documento es tipo int
return this.getDocumento() - otro.getDocumento();
}
}
Si el atributo de documento fuera un Integer también podríamos habernos apoyado en el compareTo de esa clase, ya que Integer implementa la interfaz _Comparable
En casos donde debamos comparar objetos de clases que no implementan la interfaz Comparable, o aún si lo hicieran y queremos utilizar otro criterio de orden diferente al orden natural definido en el compareTo, podemos utilizar la interfaz Comparator.
public interface Comparator<T> {
int compare(T o1, T o2);
}
Esta interfaz provee un método compare que recibe dos objetos por parámetro y los compara de forma similar que el compareTo. Deberá retornar 0 si son iguales, un entero negativo si el primero es menor al segundo, y un entero positivo si el primero es mayor.
Veamos cómo se implementa la comparación en la clase Integer.
public int compareTo(Integer anotherInteger) {
return compare(this.value, anotherInteger.value);
}
public static int compare(int x, int y) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
Notemos que Integer define en el método estático compare la relación de orden, pero no tiene relación con el compare de Comparator porque no es subtipo de ella. Luego en el compareTo (recordemos que esta clase sí implementa Comparable) invoca al método estático.
Ahora veamos cómo podríamos utilizar Comparator para proveer otro criterio de orden en nuestra clase Persona.
class ComparadorEdad implements Comparator<Persona> {
public int compare(Persona persona1, Persona persona2) {
// Asumiendo que edad es tipo Integer
return persona1.getEdad().compareTo(persona2.getEdad());
}
}
El nuevo ComparadorEdad implementa el método compare que nos exige la interfaz Comparator y nos apoyamos en el compareTo de la clase Integer para ordenar eventualmente las personas por su edad. Para utilizar el comparador podríamos invocar el método Arrays.sort que acepta como segundo argumento un objeto de tipo Comparator.
import java.util.Arrays;
public static void main(String[] args) {
Persona[] personas = new Persona[10];
// Se agregan personas al arreglo...
Arrays.sort(personas, new ComparadorEdad());
}
En este ejemplo, el método Arrays.sort invocará en su implementación el método compare de nuestro ComparadorEdad para ordenar los elementos del arreglo personas.
Definir una relación de orden en nuestras clases a través de estas interfaces es de gran utilidad para aprovechar los algoritmos de ordenamiento que incluye Java en sus librerías.
Similar a la igualdad, existen dos tipos o formas de copias de objetos.
La copia superficial (shallow copy) implica duplicar un objeto, pero no necesariamente duplicar todos los objetos que contiene como atributos. Se crea una nueva instancia del objeto a copiar, pero las referencias a objetos internos siguen siendo las mismas. Esto significa que, si modificamos un objeto interno en la copia superficial, este cambio se reflejará en el objeto original y viceversa. Es como tener dos objetos diferentes que comparten algunas partes en común.
Supongamos que tenemos una clase Persona y queremos hacer una copia superficial de un objeto Persona. Código completo en carpeta src.
class Persona {
private String nombre;
private int edad;
private Documento documento;
// Constructor y métodos...
public void setEdad(int edad) {
this.edad = edad;
}
public void setNroDocumento(int nroDocumento) {
this.documento.setNumero(nroDocumento);
}
public Persona copiaSuperficial() {
Persona copia = new Persona();
copia.nombre = this.nombre;
copia.edad = this.edad;
copia.documento = this.documento; // Se asigna la misma referencia original
return copia;
}
}
public class EjemploCopiaSuperficial {
public static void main(String[] args) {
Persona juana = new Persona("Juana", 22, 12345678);
Persona copiaJuana = juana.copiaSuperficial();
System.out.println(juana); // Juana, 22, 12345678
System.out.println(copiaJuana); // Juana, 22, 12345678
// Modificar un atributo primitivo no afecta al original
copiaJuana.setEdad(33);
// Modificar un atributo referencia sí afecta al original
copiaJuana.setNroDocumento(87654321);
System.out.println(juana); // Juana, 22, 87654321
System.out.println(copiaJuana); // Juana, 33, 87654321
}
}
En este ejemplo, creamos un método copiaSuperficial() en la clase Persona que crea una nueva instancia y copia solo los valores de los campos, no clona/copia los objetos que componen a Persona. Por lo tanto, se mantienen las mismas referencias que en el objeto original. La modificación de la edad en copiaJuana no afecta a la edad de juana porque es un campo primitivo (int), pero el campo documento es una referencia. Al modificar el estado de documento se ve reflejado ese cambio en objeto original y copia.
Se debe tener mucho cuidado con este tipo de copia porque es muy probable que introduzca defectos y comportamiento no deseado, provocando seguramente errores de tipo NullPointerException.
La copia profunda (deep copy) implica clonar no solo el objeto principal sino también todos los objetos internos de manera recursiva. Esto asegura que no haya ninguna relación de referencia compartida entre el objeto original y su copia. En otras palabras, los objetos duplicados son completamente independientes.
En Java existe una interfaz java.util.Cloneable que permite indicar que una clase puede ser clonada, lo que significa que puede realizar copias de objetos de esa clase. Sin embargo, es importante tener en cuenta que Cloneable no tiene métodos propios. Solo actúa como una marca para informar a la JVM que la clase es clonable. Para realizar una copia profunda a través de Cloneable, debemos seguir algunos pasos específicos:
public class Persona implements Cloneable {...}
@Override
public Persona clone() throws CloneNotSupportedException {...}
El método Object.clone() propone que las siguientes condiciones sean verdaderas:
x.clone() != xx.clone().getClass() == x.getClass()x.clone().equals(x)
@Override
public Persona clone() throws CloneNotSupportedException {
Persona copia = (Persona) super.clone(); // Copia superficial de Persona
copia.documento = this.documento.clone(); // Copia profunda de Documento
return copia;
}
En este último paso se realiza primero la clonación invocada al método de Object (la superclase de Persona), lo cual genera una nueva instancia de tipo Persona (como el tipo de retorno de clone devuelve Object, debemos castearlo a Persona). Luego se realiza el clonado del objeto del atributo documento para obtener su copia independiente. La sobreescritura de nuestro clone devuelve el tipo Persona en lugar de Object, y esto es válido porque Java soporta tipos de retorno covariantes.
Lamentablemente, debemos contemplar que el método clone() tiene declarada la excepción CloneNotSupportedException, la cual es checked (veremos más adelante excepciones). Por tal motivo debemos también declararla cuando sobreescribimos el método, mejor aún, capturarla dentro de nuestra implementación.
try {
Persona copia = (Persona) super.clone(); // Copia superficial de persona
copia.documento = this.documento.clone(); // Copia profunda de Documento
return copia;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
De esta forma, dado que nunca debería producirse la excepción CloneNotSupportedException porque Persona es Cloneable, documentamos esa imposibilidad lanzando la excepción AssertionError si eso es violado por error.
El ejemplo de implementación completo se encuentra en la carpeta src.
Es cierto que la implementación de la copia profunda a través de la interfaz Cloneable introduce varias complejidades que se mencionan en el item 13 del libro de Bloch. Una alternativa que propone es generar un constructor de copia que reciba por parámetro otro objeto de la misma clase para inicializar la nueva instancia clonada, o bien, utilizar un método estático de construcción de copia a partir de otro objeto de misma clase que se pase por parámetro. Ambas opciones deben contemplar la copia real de los atributos con referencias para evitar el problema de la copia superficial.
// Constructor de copia
public Persona(Persona original) {...};
// Método de construcción de copia
public static Persona copiaProfunda(Persona original) {...};
Lectura de interés:
- Item 13: Override clone judiciously - Effective Java 3rd, de Joshua Bloch.
- Java Tutorials: Ordenando objetos
Dadas las clases Persona y Documento:
a) Implementar el clonado de personas a través de la estrategia de constructor de copia.
a) Implementar el clonado de personas a través de la estrategia de método de construcción de copia.