Índice
- Introducción
- Entorno
- Ojo con el método equals
- Hagámoslo fácil, usemos el IDE
- Hagámoslo más fácil, usemos commons-lang
- Usando Hibernate, comienzan los problemas
- Conclusiones
- 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 necesario 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:
|
Java
|
|
|---|---|
|
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:
|
Java
|
|
|---|---|
|
@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 tienen 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. Entre estas ayudas vamos a encontrar métodos para implementar equals y hashCode por reflexión.
De esta forma nuestros métodos quedarían:
|
Java
|
|
|---|---|
|
@Override
public int hashCode() { return HashCodeBuilder.reflectionHashCode(this); } @Override public boolean equals(Object obj) { return EqualsBuilder.reflectionEquals(this, obj); } |
|
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 inicializació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:
|
Java
|
|
|---|---|
|
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:
|
Java
|
|
|---|---|
|
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.
6.2. Accediendo directamente a los atributos
Tanto en el código del estilo:
|
Java
|
|
|---|---|
|
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 a 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 objeto con otro cuyos atributos están a null.
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.
6.3. Otros problemas asociados a las relaciones entre objetos persistentes
Imaginemos que la clase Person tiene un atributo que es una lista de Phone:
|
Java
|
|
|---|---|
|
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:
|
Java
|
|
|---|---|
|
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 PersistentBag que tiene atributos diferentes a los de un ArrayList normal, por lo que 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 vayamos comparando.
6.4. ¿Qué 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.
Por ejemplo:
|
Java
|
|
|---|---|
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ía quedar de la siguiente manera:
|
Java
|
|
|---|---|
|
@Entity
public class Person { private Integer id = null; private String name = null; private List phones = 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 setPhones(List phones) { this.phones = phones; } @Override public int hashCode() { final HashCodeBuilder hcb = new HashCodeBuilder(); hcb.append(getName()); 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()); 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) { isEquals = false; } return isEquals; } } |
|
7. Conclusiones
Hemos visto cómo 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 qué 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)
alejandropg@autentia.com
http://www.autentia.com
3 respuestas
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.
Muy interesante de programación me interesa mucho aprender