Saltar a contenido

Necesidad de Java Equals y Hashcode

Los métodos Java equals() y hashCode() están presentes en la clase Object. Como todas las clases heredan de la clase Object de forma automática reciben una implementación predeterminada de equals() y hashCode() sino se sobreescribe.

Equals

El método equals() sirve para comparar instancias de clases entre sí. Si usamos colecciones como las que hemos visto en el tema: ArrayList, LinkedList... muchos de sus métodos (contains, remove, indexOf, etc.) llaman al método equals internamente para encontrar el objeto. Es decir, van comparando el objeto con los que hay en la lista.

A veces cuando creamos colecciones donde el tipo de dato es un Objeto definido por nosotros, como Persona, Coche, etc. Es importante recordar, que comparar con ==, estamos comparando direcciones de memoria en el caso de variables de instancia

alt text

Por defecto, el método equals tiene el siguiente definición, en que dos variables de instancia son iguales si apuntan al mismo objeto

Lo heredamos automáticamente de Object de la siguiente forma:

//Dos objetos son iguales, si son el mismo objeto
public boolean equals(Object obj) {
    return (this == obj);
}

Necesitamos sobrescribir el método equals para la clase que creamos para indicar cuando dos objetos de una clase son iguales. Por ejemplo, para una clase Persona puede ser cuando tienen el mismo dni

public class Persona {
    private String dni;
    private String nombre;
    private int edad;
    private double altura;

    // Constructor
    public Persona(String dni, String nombre, int edad, double altura) {
        this.dni = dni;
        this.nombre = nombre;
        this.edad = edad;
        this.altura = altura;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Persona persona = (Persona) o;
        //comparamos el dni mediante el equals definido en String
        return Objects.equals(dni, persona.dni);
    }    
}
Volvemos a recordar lo siguiente:

public static void main(String[] args) {
    Persona pepe = new Persona("12345678A", "Pepe", 25, 1.75);
    Persona pepe2 = new Persona("12345678A", "Pepe", 25, 1.75);
    Persona maria = new Persona("87654321B", "María", 30, 1.60);        
    Persona pepe3 = pepe;
    Persona jose = new Persona("12345678A", "Jose", 25, 1.75);

    System.out.println(pepe==pepe3);//true, son el mismo objeto
    System.out.println(pepe==pepe2);//false, no son el mismo objeto
    System.out.println(pepe.equals(pepe2));//true, son distintos objetos pero tienen el mismo dni        
    System.out.println(pepe.equals(maria));//false, no tienen el mismo dni
    System.out.println(pepe.equals(jose));//true, no son el mismo objeto pero tienen el mismo dni

}

De esta forma, una vez definido equals, podemos localizar objetos en listas. Ya que utiliza internamente equals

ArrayList<Persona> personas = new ArrayList<>();
personas.add(pepe);
personas.add(maria);
System.out.println(personas.contains(jose));//true
System.out.println(personas.indexOf(jose));//0

En el siguiente ejemplo tenemos la clase Client, en nuestra lógica dos objetos de cliente son iguales si son del mismo tipo (Client) y comparten el mismo nombre.

Client.java
public class Client {

    private String name;
    private int age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Client client = (Client) o;
        return Objects.equals(name, client.name);
    }
}

Contrato equals() - hashcode()

Cuando utilicemos estructuras de datos basadas en hash tables como HashMap, HashSet, LinkedHashMap, LinkedHashSet, Hashtable, ... necesitamos de un contrato equals - hashcode:

Cuando sobreescribimos el método equals() en nuestra clase para cambiar la lógica que se hereda de la clase Object, tenemos que sobrescribir el método hashCode() de manera que si dos instancias de nuestra clase son iguales según la nueva lógica en equals(), el método hashCode() deberá retornar el mismo valor si lo llamo para dichas instancias.

Es necesario tener en cuenta, que estas estructuras de datos utilizan tablas Hash para localizar y organizar la información de forma muy rápida. Por lo que la función Hash tiene que saber cuando dos objetos son el mismo.

alt text alt text

El método hashcode() sirve para comparar objetos de una forma más rápida en estructuras Hash ya que únicamente nos devuelve un número entero. Cuando Java compara dos objetos en estructuras de tipo hash primero invoca al método hashcode y luego el equals. Si los métodos hashcode de cada objeto devuelven diferente hash no seguirá comparando y considerará a los objetos distintos. En el caso en el que ambos objetos compartan el mismo hashcode Java invocará al método equals() y revisará si se cumple la igualdad.

Ejemplo

En el siguiente ejemplo, definimos que el nombre es elemento de igualdad de dos objetos y también para definir la función Hash

Cliente.java
public class Cliente {

    private String name;
    private int age;

    public Cliente(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Cliente cliente = (Cliente) o;
        return Objects.equals(name, cliente.name);
    }
    //la función hash localiza el objeto por su nombre
    @Override
    public int hashCode() {
        return Objects.hashCode(name);
    }

    @Override
    public String toString() {
        return "Cliente{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
De esta forma, es necesario tenerlo en cuenta cuando trabajamos con estructuras que necesitan la función Hash

public static void main(String[] args) {
    Cliente pepe1=new Cliente("Pepe", 25);
    Cliente maria1=new Cliente("María", 30);
    Cliente pepe2=new Cliente("Pepe", 45);

    //conjunto. No hay repeticiones según función Hash
    Set<Cliente> clientes=new HashSet<>();
    clientes.add(pepe1);
    clientes.add(maria1);
    //"Pepe" ya existe y no lo añade
    clientes.add(pepe2);
    System.out.println(clientes);
    //[Cliente{name='María', age=30}, Cliente{name='Pepe', age=25}]

    //con un HashMap, Cliente es la Key, y utiliza la función Hash para localizar el elemento
    Map<Cliente,Integer> compras=new HashMap<>();
    compras.put(pepe1, 100);
    compras.put(maria1, 200);
    //en este caso, la clave sigue teniendo la edad de 25 y no de 45. Si quisieramos que fueran objetos diferentes
    //tendríamos que cambiar la función Hash
    compras.put(pepe2, 300);
    System.out.println(compras);
    //{Cliente{name='María', age=30}=200, Cliente{name='Pepe', age=25}=300}
}

Para que podáis comprobar la importancia de la función Hash, si modificamos la función hashCode por por una función forzada que devuelve un número aleatorio, los objeto son ilocalizables

   Cliente pepe1=new Cliente("Pepe", 25);
    Cliente maria1=new Cliente("María", 30);
    Cliente pepe2=new Cliente("Pepe", 45);

    //conjunto. No hay repeticiones según función Hash. Pero esta función es incorrecta
    Set<Cliente> clientes=new HashSet<>();
    clientes.add(pepe1);
    clientes.add(maria1);

    clientes.add(pepe2);
    System.out.println(clientes);
    //[Cliente{name='Pepe', age=25}, Cliente{name='María', age=30}, Cliente{name='Pepe', age=45}]
    System.out.println(pepe1.hashCode());//56(aleatorio)
    System.out.println(pepe1.hashCode());//45(aleatorio)
    System.out.println(clientes.contains(pepe1));//false. no lo encuentra
    System.out.println(clientes.size());//3

Estaremos calculando al azar el hashcode y dos objetos iguales devolverán hashcodes diferentes. El HashSet nos devolverá false cuando invoquemos el método contains aunque sabemos que el elemento existe en el conjunto. Y también agregará duplicado ya que no encuentra el elemento.