Java tiene gravísimos puntos oscuros desde sus orígenes, debido a que la API y el lenguaje están muy orientados a la implementación de la JVM y a la compatibilidad hacia atrás en lugar de orientarse a facilitar un lenguaje claro y sin ambigüedades.
Baste decir que cuando hablamos de J2EE no hablamos de “Java v2 Enterprise Edition” sino de “Java v1.2 Enterprise Edition“, pero todo el mundo (incluido yo) utiliza J2EE en lugar de simplemente JEE o J7EE (para referirnos a la versión 1.7 que es la actual de Java). Por cierto, cuánta falta hace que llegue por fin una versión 2.0 de Java!!!
Una de estas áreas de mejora es el mal uso de un concepto tan común como “duplicar” un objeto. La mayoría de las pesonas que leen este código,
1
2
3
| Person personA= new Person();
personA.setName("Juan");
Person personB = personA; |
piensan que personB es una copia de personA, dos objetos diferentes pero con los mismos valores, porque los humanos interpretamos la línea 3 como “Crear un nuevo objeto del tipo Person llamado personB que inicalizamos con los valores que tiene el objeto llamado personA”, o simplemente decimos “Copiar personA a un nuevo objeto del tipo Person llamado personB” pero queremos decir “Duplicar” en lugar de copiar.
Sin embargo Java se comporta de un modo diferente al esperado ya que la JVM interpreta esa línea de diferente forma. Si ahora realizamos un cambio en personB, comprobaremos asombrados cómo personA también ha cambiado!!!
4
5
| personB.setName("Luisa");
//En este punto personA.getName() devolería "Luisa" y no "Juan". |
¿Por qué? Porque lo que realmente significa para la JVM la conflictiva línea 3 anterior, es “Crear una nueva variable del tipo Person llamada personB y asginar la misma referencia que tiene la variable personA“, o simplemente “Copiar la referencia (y no el contenido) de personA en una nueva variable del tipo Person llamada personB“. El resultado es que tenemos dos variables apuntando a una única referencia (espacio en memoria) en lugar de dos variables independientes con sus respectivas y diferentes referencias (espacios de memoria). Así, cuando una de las dos variables cambia, cambian las dos. Sí, Java se orienta más a punteros que a objetos, que es más útil en programación a bajo nivel y para ahorra unos bits; no en vano Java se basó en C.
Entonces, Java no facilita una funcionalidad para duplicar objetos?!?!?! Java ofrece un interfaz Clonable, pero no su imlementación. Ocurre lo mismo para comparar dos objetos o comprobar si dos objetos son iguales o imprimir los valores de un objeto, te obliga a escribirte tu propia implementación para estas funciones de uso común, más común (para un desarrollador) que las interpretaciones estándares, ralentizando el desarrollo al escribir código de poco valor añadido y propenso a los bugs pero sobre todo complicando el mantenimiento ya que tiene que evolucionar al tiempo que evoluciona el negocio.
O utilizar implementaciones genéricas que son peligrosas.
Duplicación de objetos genérica
Podemos implementar nuestra propia solución genérica para solucionar esta laguna de Java, o podemos utilizar soluciones ya existentes. Así org.apache.commons.beanutils.BeanUtils parece ofrecernos la solución:
Person personB = null;
try {
personB = (Person) BeanUtils.cloneBean(personA);
} catch (Exception e) {
e.printStackTrace();
} |
En este caso funcionaría, y ahora al volver a ejecutar un cambio en personB no cambiamos personA.
Pero qué pasa si el objeto Person tiene a su vez otros objetos, como por ejemplo un objeto User?
@Test public void duplication2Test2 () {
User userA;
Person personA, personB;
personA = new Person();
personA.setName("nameA");
userA = new User();
userA.setUsername("usernameA");
personA.setUser(userA);
personB = personDuplication.duplication2(personA);
personB.setName("nameB");
personB.getUser().setUsername("usernameB");
System.out.println("duplication1Test - personA.name = " + personA.getName());
System.out.println("duplication1Test - personB.name = " + personB.getName());
System.out.println("duplication1Test - personA.user.username = " + personA.getUser().getUsername());
System.out.println("duplication1Test - personB.user.username = " + personB.getUser().getUsername());
Assert.assertEquals(personA.getName(), "nameA");
Assert.assertEquals(personB.getName(), "nameB");
Assert.assertEquals(personA.getUser().getUsername(), "usernameA");
Assert.assertEquals(personB.getUser().getUsername(), "usernameB");
} |
Este Test falla. La razón es simple, BeanUtils duplica las variables primitivas y las variables de tipos de Java como Boolean, Integer o String (que es la que se utiliza en el ejemplo), pero en lugar de duplicar los objetos que nosotros implementamos (como el usado en el ejemplo User) está copiando su referencia.
Duplicación profunda de objetos genérica
Una forma sencilla es serializar nuestros objetos y desearializarlos en nueva instancia, para lo que podemos utilizar org.apache.commons.lang.SerializationUtils. La parte negativa de esta solución es que nuestros objetos deben ser serializables (implementar el intefaz Serializable), y también todos los objetos que los componen, y también todos los objetos que componen a estos, y también… por lo cual es una solución con peligros de cara al mantenimiento evolutivo.
Por fin, la siguiente implementación pasaría el test anterior:
public Person duplicationPerson(Person personSource) {
Person personTarget = null;
try {
personTarget = (Person) SerializationUtils.clone(personSource);
} catch (Exception e) {
e.printStackTrace();
}
return personTarget;
} |
Lo podríamos hacer más genérico sustituyendo el tipo Person por Serializable. Más potencia, más peligro.