Creación: 13-01-2008
Índice de contenidos
1.Introducción
2.Entorno
3.Ojo con el método equals
4.Hagámoslo fácil, usemos el IDE
5.Hagámoslo más fácil, usemos commons-lang
6.Usando Hibernate, comienzan los problemas
6.1.Identificación del tipo del objeto con el que comparamos
6.2.Accediendo directamente a los atributos
6.3.Otros problemas asociados a las relaciones entre objetos persistentes
6.4.¿Que pasa con el id de persistencia?
6.5.Una solución a todos los problemas
7.Conclusiones
8.Sobre el autor
1. Introducción
En la clase java.lang.Object
(y por lo tanto, o mejor dicho, y por herencia, en todas las demás clases) tenemos dos métodos que suelen ser dos grandes olvidados:
public boolean equals(Object obj)
public int hashCode()
Estos métodos son especialmente importantes si vamos a guardar nuestros objetos en cualquier tipo de colección: listas, mapas (aquí es especialmente importante el método
hashCode
), … y más aun si los objetos que vamos a guardar en la colección son serializables. En este último caso es especialmente necesarios reescribir estos métodos ya
que la implementación proporcionada por la clase Object trabaja con las referencias, y si un objeto lo serializamos y lo deserializamos, tendrá diferente referencia, sin embargo
seguirá siendo el mismo objeto. Es decir, si tengo un objeto persona donde tengo guardados mis datos, y serializo este objeto a un fichero y luego lo vuelvo a leer, tendré una instancia diferente (un nuevo objeto con diferente referencia), pero seguiré siendo “yo”.
Estos métodos van a permitir encontrar un objeto en una colección (equals
) o localizar el objeto en un mapa (hashCode
).
Se debe cumplir que, si dos objetos son iguales según el método equals
, deben generar el mismo entero al llamar a hashCode
(si no son iguales pueden producir el mismo entero, pero es aconsejable que generen enteros diferentes para aumentar el rendimiento de los mapas).
De hecho muchos puristas consideran que una clase no está correctamente implementada si no tiene debidamente sobreescritos estos métodos. Con esto os podéis hacer una idea de la importancia de estos métodos.
En este tutorial vamos a ver algunas importantes consideraciones a tener en cuenta cuando sobreescribimos estos métodos, especialmente si vamos a trabajar con Hibernate o con cualquier otro sistema que genere proxys de nuestras clases de forma dinámica en tiempo de ejecución (por ejemplo con el uso de aspectos).
2. Entorno
El tutorial está escrito usando el siguiente entorno:
- Hardware: Portátil Asus G1 (Core 2 Duo a 2.1 GHz, 2048 MB RAM, 120 GB HD).
- Sistema Operativo: GNU / Linux, Debian (unstable), Kernel 2.6.23, KDE 3.5
- JDK 1.5.0_14 (instalada con el paquete sun-java5-jdk de Debian)
- Eclipse Europa
3. Ojo con el método equals
Al reescribir el método equals
, tenemos que fijarnos bien en que el parámetro de entrada es de tipo Object
. Es decir el método siempre debe ser: public boolean equals(
Object
obj)
.
Es un descuido muy común usar como parámetro de entrada una instancia de la misma clase donde se está reescribiendo el método, por ejemplo, para la clase Person
: public boolean equals(Person obj)
. Esto es un error ya que no estamos sobreescribiendo el método equals
de nuestro padre, sino que estamos creando un nuevo método, con el mismo nombre, pero con parámetros diferentes. Esto provoca que cuando se invoque el método equals
intentando hacer uso del polimorfismo (por ejemplo en las colecciones) no se llame al método que hemos escrito nosotros, sino a la implementación por defecto proporcionada por la clase Object
.
4. Hagámoslo fácil, usemos el IDE
Actualmente, prácticamente todos los IDEs (entornos integrados de desarrollo) tienen alguna manera de generar de forma automática estos métodos. Por ejemplo en Eclipse podemos hacer: Source –> Generate hashCode() and equals()… –> marcamos los atributos que queremos que se tengan en cuenta en estos métodos.
Importante: Siempre deberíamos tener en cuenta los mismos atributos para calcular el equals
y para calcular el hashCode
. Esta regla es válida independientemente de si generamos estos métodos de forma automática o los escribimos a mano.
Por ejemplo para la siguiente clase:
public class Person implements Serializable { private String name; private String surname; private int age; ... }
Deberíamos obtener la siguiente implementación si elegimos todos los atributos:
@Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + age; result = prime * result + ((name == null) ? 0 : name.hashCode()); result = prime * result + ((surname == null) ? 0 : surname.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; final Person other = (Person)obj; if (age != other.age) return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; if (surname == null) { if (other.surname != null) return false; } else if (!surname.equals(other.surname)) return false; return true; }
Estas implementaciones tiene el inconveniente de que cada vez que añadimos o quitamos un atributo hay que volver a generar el método o modificarlo a mano.
5. Hagámoslo más fácil, usemos commons-lang
Apache nos proporciona la librería commons-lang
(http://commons.apache.org/lang/).
En esta librería podemos encontrar multitud de ayudas de uso muy común en cualquier aplicación (os recomiendo que le echéis un buen vistazo). Entre estas ayudas vamos a encontrar métodos para implementar equals
y hashCode
por reflexión.
De esta forma nuestros métodos quedarían de la siguiente forma:
@Override public int hashCode() { return HashCodeBuilder.reflectionHashCode(this); } @Override public boolean equals(Object obj) { return EqualsBuilder.reflectionEquals(this, obj); }
De esta forma no sólo escribimos menos, sino que podemos cambiar los atributos de la clase sin preocuparnos de estos métodos.
Importante: Podemos tener inconvenientes si usamos un SecurityManager, ya que esto podría causar una excepción al intentar acceder por reflexión a los atributos privados de la clase.
6. Usando Hibernate, comienzan los problemas
Con las dos implementaciones que hemos visto vamos a encontrarnos con problemas si usamos Hibernate (o EJB 3), si nuestra clase es una clase persistente. Prácticamente todos los problemas que se van a dar vienen provocados porque en varias ocasiones Hibernate no trabaja con nuestra clase directamente, sino con un proxy que genera dinámicamente en tiempo de ejecución. Por ejemplo: si usamos el método load
para recuperar un objeto, en el caso de relaciones con inicilización “lazy”, …
6.1. Identificación del tipo del objeto con el que comparamos
Una de las primeras cosas que siempre se hace en el equals
es mirar si el tipo del objeto que nos pasan como parámetro es igual que nuestro tipo. Lo veíamos con el código:
if (getClass() != obj.getClass()) return false; final Person other = (Person)obj;
Esto nos va a dar problemas con Hibernate, ya que como crea un proxy, las clases no van a ser realmente las mismas. Por esto debemos usar la sentencia instanceof
, de esta forma:
if (!(obj instanceof Person)) return false; final Person other = (Person)obj;
Aquí no habrá problema porque el proxy que crea Hibernate extiende de nuestra clase Person
, así que si preguntamos si una instancia del tipo del proxy creado por Hibernate es “instanceof
” Person
, la respuesta será true
(el tipo también se hereda en un relación de herencia).
6.2. Accediendo directamente a los atributos
Tanto en el código del estilo:
if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false;
como en lo que hace internamente la clase EqualsBuilder
, se accede directamente al valor de los atributos; es decir, no se usan los getter correspondientes.
Esto también nos puede dar problemas si hemos recuperado el objeto mediante el método load
de la clase Session
. El problema viene provocado porque al cargar el objeto con load
, Hibernate lo que devuelve es un proxy. Este proxy sirve para que no se acceda al la base de datos hasta que realmente se acceda al objeto; es decir, hasta que no se llame a alguno de los getters no se recuperan los valores de la base de datos, por lo que, en nuestro ejemplo, name
y surname
valdrán null
.
De esto se deduce que si comparamos un objeto con otro que acabamos de recuperar con load
, el resultado del método equals
será false
, aunque los objetos sean iguales, ya que estaremos comparando un objetos con otro cuyos atributos están a null
porque todavía no se han recuperado de la base de datos.
Este mismo problema lo vamos a tener con las relaciones entre objetos con inicialización “lazy”, es decir, hasta que no se llama al getter no se recupera la colección.
Para solucionar esto deberíamos garantizar que el acceso a los atributos se haga mediante los getters (veremos la solución un poco más adelante).
6.3. Otros problemas asociados a las relaciones entre objetos persistentes
Imaginemos que la clase Person
tiene un atributo que es una lista de Telefonos: private List<Phone> phones;
La clase Phone
también se persiste mediante Hibernate, así que tenemos una relación de 1 a n entre Person
y Phone
.
Si en el equals
de Person
hacemos lo siguiente:
if (!this.getPhones().equals(other.getPhones())) return false;
nos vamos a encontrar con un problema: el resultado del equals
va a ser false
, a pesar de estar usando los getters como dijimos en el caso anterior.
Esto se debe a que cuando Hibernate recupera una relación de este tipo usa sus propias implementaciones de las colecciones. En este caso Hibernate nos puede estar devolviendo un PersistenBag
que tiene atributos diferentes a los de un ArrayList
normal (por ejemplo tiene un atributo que identifica que extremo de la relación es el propietario de la misma), por lo que el la comparación fallará.
Para evitar esto, lo mejor es que en la implementación del método equals
de Person
, recorramos uno a uno los elementos de la lista y los vallamos comparando (veremos la solución un poco más adelante).
6.4. ¿Que pasa con el id de persistencia?
Hibernate nos recomienda que usemos un identificador “artificial” para los objetos persistentes, es decir un id que no tenga nada que ver con los datos de negocio (si luego queremos hacer accesos por los campos de negocio basta con hacer índices sobre ellos). De esta manera en la clase Person
nos aparecerá un atributo del estilo:
private Integer id;
Este atributo nunca habrá que tenerlo en cuenta en el equals
(y por lo tanto tampoco en el hashCode
).
En un principio podemos estar tentados a usarlo ya que si dos objetos tienen el mismo id, es que son el mismo. Pero debemos recordar que un objeto sólo tiene id si ya ha sido persistido. Es decir podríamos estar comparando un objeto ya persistido con otro que todavía no ha sido persistido; estos dos objetos podrían ser iguales, pero el segundo todavía no tiene id, por lo que la comparación devolvería false
.
6.5. Una solución a todos los problemas
Teniendo en cuenta todos los casos comentados anteriormente, la clase Person
nos podrían quedar de la siguiente manera:
@Entity public class Person { private Integer id = null; private String name = null; // ... otros atributos private List phone = new ArrayList(); @Id @GeneratedValue @Column(unique = true, nullable = false) public Integer getId() { return id; } /** Para uso de Hibernate. */ @SuppressWarnings("unused") private void setId(Integer id) { this.id = id; } @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.ALL }) @JoinColumn(name = "phone_id", referencedColumnName = "id") public List getPhones() { return phones; } /** Para uso exclusivo de Hibernate. */ @SuppressWarnings("unused") private void setRecordFiles(List recordFiles) { this.recordFiles = recordFiles; } // ... otros getter y setter @Override public int hashCode() { final HashCodeBuilder hcb = new HashCodeBuilder(); hcb.append(getName()); // ... otros atributos, pero no el id for (Phone phone : phones) { hcb.append(phone); } return hcb.toHashCode(); } @Override public boolean equals(Object obj) { boolean isEquals = false; try { final Person other = (Person)obj; final EqualsBuilder eqb = new EqualsBuilder(); eqb.append(getName(), other.getName()); // ... otros atributos, pero no el id if (getPhones().size() != other.getPhones().size()) { return false; } for (int i = 0; i < phones.size(); i++) { eqb.append(phones.get(i), other.getPhones().get(i)); } isEquals = eqb.isEquals(); } catch (Exception e) { // Sobre todo si no se puede hacer la conversión de tipos, // y en general si se produce cualquier error. isEquals = false; } return isEquals; } }
7. Conclusiones
Hemos visto como algo aparentemente inocente se puede convertir en un quebradero de cabeza. Por eso desde Autentia (http://www.autentia.com) siempre os animamos a que profundicéis en las tecnologías que usáis, y que implicaciones tienen. Y os intentamos facilitar las cosas con este tipo de tutoriales.
Espero que os haya servido de ayuda y que aclare un poco las cosas.
8. Sobre el autor
Alejandro Pérez García, Ingeniero en Informática (especialidad de Ingeniería del Software)
Socio fundador de Autentia (Formación, Consultoría, Desarrollo de sistemas transaccionales)
mailto:alejandropg@autentia.com
Autentia Real Business Solutions S.L. – “Soporte a Desarrollo”
como se puede utilizar el metodo equals cuando haces comprobaciones del login usuarios
Si tus credenciales son usuario/clave lo que no tienes que hacer nunca es guardar la clave en texto claro, siempre la tienes que guardar con un cifrado asimétrico. Es decir tienes que usar un cifrado que no sea reversible, de forma que aunque consigan los hashes de las claves guardados en la base de datos no se pueda revertir el proceso para obtener la clave original.
Ahora con esta idea lo que puedes hacer es una clase que represente esas credenciales (usuario/clave).
Esta clase podría recibir en su constructor la clave en texto claro y en el propio constructor hacer el cifrado y obtener el hash que guardas en un atributo de la clase. Ahora podrías guardar esa clase en la base de datos, ya que lo que estas guardando es la clave ya cifrada. Y para hacer el equals, como lo haces sobre el atributo (que ya es el hash), te funcionar sin problema si lo comparas con unas credenciales guardadas en tu base de datos.