Cómo escribir iguales () en Java

Feb 22 2023
Al final de este artículo, se cubrirá toda la información necesaria para poder escribir el mejor método equals() en el lenguaje de programación Java. Los detalles de la igualdad se tratarán en términos de definición matemática y a la manera del lenguaje de programación Java para cubrir la información fundamental que se necesita para usar la mejor práctica del método equals().

Al final de este artículo, se cubrirá toda la información necesaria para poder escribir el mejor método equals() en el lenguaje de programación Java. Los detalles de la igualdad se tratarán en términos de definición matemática y a la manera del lenguaje de programación Java para cubrir la información fundamental que se necesita para usar la mejor práctica del método equals( ) .

Definición matemática de igualdad

Según Wikipedia , la definición de igualdad matemática es

En matemáticas, la igualdad es una relación entre dos cantidades o, más generalmente, dos expresiones matemáticas, afirmando que las cantidades tienen el mismo valor, o que las expresiones representan el mismo objeto matemático. La igualdad entre A y B se escribe A = B , y se pronuncia A igual a B .

Definición de Igualdad en Java

En el lenguaje de programación Java, la igualdad significa simplemente tener el mismo valor. A veces, literalmente, significa que tiene el mismo valor y, a veces, tiene la misma estructura en términos de las propiedades del objeto.

Cuando se necesita verificar la igualdad de dos valores de tipo primitivo, el signo igual se usa directamente como en el ejemplo a continuación;

int a = 5;
int b = 6;
int c = 5;

// Prints "false" since a and b do not have the same value.
System.out.println(a == b);

// Prints "true" since a and c have the same value.
System.out.println(a == c);

Por otro lado, a veces es necesario verificar la igualdad de dos objetos. En términos de objetos, existen dos enfoques diferentes que se utilizan en diferentes etapas del proceso de comprobación de la igualdad de dos objetos. Entremos en los detalles.

Igualdad Referencial

En el lenguaje de programación Java, los valores reales de los objetos se almacenan en algún lugar de la memoria y están señalados por las referencias de objetos que también se almacenan en algún lugar de la memoria. Veamos el siguiente fragmento de código;

// Create an object with "new" operator.
// Point to object with object reference "a".
Object a = new Object();

// Create another object reference.
// Point to the same object as the reference "a" is pointing.
Object b = a;

// Output is true.
System.out.println(a == b);

// Create another object with "new" operator.
// Point to object with object reference "b".
b = new Object();

// Output is false.
System.out.println(a == b);

En tal escenario, "a" y "b" son en realidad dos referencias que apuntan al mismo objeto, por lo que se puede decir directamente que "a" es igual a "b" ya que se obtiene la igualdad referencial.

Sin embargo, el escenario opuesto no es válido todo el tiempo. Dos referencias diferentes pueden apuntar a ubicaciones de memoria diferentes, pero los objetos a los que apuntan las referencias pueden seguir siendo iguales.

En la vida normal, a veces se comparan cantidades. Por ejemplo, si tienes dos manzanas y tu amigo tiene dos manzanas, entonces la cantidad de manzanas que tú y tu amigo tienen es igual.

Por otro lado, a veces las cosas también pueden compararse en términos de sus propiedades o estructura. Entremos en los detalles de este escenario.

Igualdad Estructural

La igualdad estructural es el tipo del cual los objetos se comparan según los estados de sus propiedades. Se necesita una herramienta más sofisticada para verificar este tipo de igualdad en lugar de solo el operador "==". Es el método equals(). Proviene de la clase Object que se encuentra en el punto más alto de la jerarquía de clases en el lenguaje de programación Java. Veamos el próximo fragmento de código Java;

public class Car {

    // Assume that we have corresponding constructor, accessors, mutators, etc.

    private String brand;
    private String model;
    private Integer modelYear;
    private Integer mileage;
    
}

Car car1 = new Car("Audi","R8", 2023);
Car car2 = new Car("Audi","R8", 2023);
Car car3 = new Car("Audi","R8", 2021);

System.out.println(car1.equals(car2));
System.out.println(car2.equals(car3));

public boolean equals(Object obj) {
    return (this == obj);
}

public class Car {

    private String brand;
    private String model;
    private Integer modelYear;
    private Integer mileage;

    // Assume that we have corresponding constructor, accessors, mutators, etc.

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o; 
        return brand.equals(car.brand) && model.equals(car.model) && modelYear.equals(car.modelYear);
    }
    
}

En la segunda línea comenzamos a comprobar la identidad de los objetos. Si la referencia de objeto o es nula o ambas referencias de objeto apuntan a objetos que pertenecen a clases diferentes, entonces se puede decir directamente que estos dos objetos no tienen la misma identidad y no son iguales. Si este no es el caso, finalmente podemos verificar las estructuras reales de los objetos.

En la tercera línea del método, convertimos la referencia o al tipo de clase correspondiente ya que el método se anula desde la clase Object y ahora sabemos que pertenece a la clase Car.

Finalmente, en la última línea del método, se verifica la estructura de dos objetos y esta es la línea real en la que se define la identidad. Simplemente, si las marcas, los modelos y los años del modelo son los mismos, entonces estos dos objetos son iguales. En otras palabras, esta es la igualdad estructural.

Hemos aprendido las propiedades de la igualdad matemática en la escuela primaria. Algunas de estas propiedades también deben ser válidas para el método equals() . Significa que estas propiedades deben agregarse a los ingredientes del método equals() . Veamos estas propiedades en términos de igualdad matemática y en términos de igualdad en Java;

Propiedad reflexiva

En la definición matemática de propiedad reflexiva, para todos los números reales x ;

x = x

Un número es igual a sí mismo.

En términos de igualdad de Java, un objeto tiene que ser igual a sí mismo;

Car car1 = new Car("Audi","R8",2013);
System.out.println(car1.equals(car1));

@Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Car car = (Car) o; 
        return mileage < car.mileage && brand.equals(car.brand) && model.equals(car.model) && modelYear.equals(car.modelYear);
    }

Propiedad simétrica

En términos de definición matemática de propiedad simétrica, para todos los números reales x e y ;

Si x = y , entonces y = x

No importa el orden de igualdad.

En términos de igualdad de Java, si el objeto a es igual al objeto b, entonces el objeto b tiene que ser igual al objeto a;

Car car1 = new Car("Audi","R8",2013);
Car car2 = new Car("Audi","R8",2013);
System.out.println(car1.equals(car2));
System.out.println(car2.equals(car1));

public class Car {

    private String brand;
    private String model;
    private Integer modelYear;
    private Integer mileage;

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Car)) return false;
        Car car = (Car) o;
        return brand.equals(car.brand) && model.equals(car.model) && modelYear.equals(car.modelYear);
    }
}

public class SportCar extends Car {
    
    private String engineType;

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof SportCar)) return false;
        SportCar sportCar = (SportCar) o;
        return engineType.equals(sportCar.engineType);
    }
}

Car car1 = new Car("Audi", "R8", 2012, 1000);
SportCar car2 = new SportCar("Audi", "R8", 2012, 1000, "4.0");
System.out.println(car1.equals(car2));
System.out.println(car2.equals(car1));

Propiedad transitiva

En la definición matemática de la propiedad transitiva, para todos los números reales x , y y z ;

si x = y y y = z , entonces x = z

Dos números iguales al mismo número son iguales entre sí.

En términos de igualdad de Java, si el objeto a es igual al objeto b y el objeto b es igual al objeto c, entonces el objeto a tiene que ser igual al objeto c;

SportCar car1 = new SportCar("Audi", "R8", 2012, 1000, new Turbo("100RPM"));
Car car2 = new Car("Audi", "R8", 2012, 1000);
SportCar car3 = new SportCar("Audi", "R8", 2012, 1000, new Turbo("101RPM"));
System.out.println(car1.equals(car2));
System.out.println(car2.equals(car3));
System.out.println(car1.equals(car3));

public class SportCar extends Car {
    
    private Turbo turbo;

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Car)) return false;
        if (!(o instanceof SportCar)) return o.equals(this);
        return super.equals(o) && ((SportCar) o).turbo == turbo;
    }
}

public class Turbo {
    private String power;

    public Turbo(String power) {
        this.power = power;
    }
}

SportCar car1 = new SportCar("Audi", "R8", 2012, 1000, new Turbo("100RPM"));
Car car2 = new Car("Audi", "R8", 2012, 1000);
SportCar car3 = new SportCar("Audi", "R8", 2012, 1000, new Turbo("101RPM"));
System.out.println(car1.equals(car2));
System.out.println(car2.equals(car3));
System.out.println(car1.equals(car3));

Propiedad consistente

Esta es una propiedad diferente ya que matemáticamente no existe un equivalente exacto. Para este, necesitamos obtener el mismo resultado para ejecuciones consecutivas del método equals().

Veamos el siguiente fragmento de código;

Car car1 = new Car("Audi", "R8", 2012, 1000);
Car car2 = new Car("Audi", "R8", 2012, 1000);

for (int i = 0; i < 100000; i++) {
    System.out.println(car1.equals(car2));
}

@Override
public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof Car)) return false;
    Car car = (Car) o;
    Random random = new Random();
    return brand.equals(car.brand) && model.equals(car.model) && (random.nextInt(10) + 2013) == car.modelYear;
}

Nota: Este ejemplo realmente no me satisface. Después de una extensa investigación, lo reemplazaré por uno mejor. Se dice claramente en la documentación de Java del método equals() que "múltiples invocaciones de x.equals(y)retorno consistente trueo retorno consistente false". Debe haber un ejemplo sólido de violación de esta declaración.

Propiedad de parámetro nulo

Así es como elijo llamar a esta propiedad.

Mire el fragmento de código a continuación;

Car car1 = new Car("Audi", "R8", 2012, 1000);
Car car2 = null;
System.out.println(car1.equals(car2));

Ahora está claro qué propiedades debe tener una implementación del método equals() para proporcionar un cálculo correcto de la igualdad. Además, hemos visto los posibles escenarios de violación, por lo que ahora podemos evitarlos por el bien de la calidad del código y la lógica empresarial.

Hoy en día, muchos IDE proporcionan mecanismos de generación para generar las mejores implementaciones posibles del método equals() . Es una buena idea quedarse con ellos la mayor parte del tiempo en lugar de implementar el método equals() . Veamos un ejemplo de Intellij IDEA ;

@Override
   public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;
       Car car = (Car) o;
       return brand.equals(car.brand) && model.equals(car.model) && modelYear.equals(car.modelYear) && mileage.equals(car.mileage);
   }

En la primera línea, se comprueba la igualdad referencial, porque dos referencias a objetos pueden apuntar al mismo objeto.

En la segunda línea, se garantiza la validación de la propiedad de parámetro nulo y también si la condición actúa como una cláusula de protección si al controlar el tipo de clase de los objetos que señalan las referencias para evitar la violación de la propiedad transitiva y la propiedad simétrica . .

En la tercera línea, la referencia del objeto se convierte en el tipo de clase correspondiente, que es Car en nuestro ejemplo, para llegar a las propiedades del objeto Car.

Finalmente, en la cuarta línea se comprueba la igualdad en términos de igualdad estructural.

Hay otras plantillas para la generación del método equals() . También puedes ir y comprobarlos.

Si desea escribir la implementación por su cuenta, tenga en cuenta que la implementación debe garantizar la validez de todas las propiedades del método equals() .

A medida que llegamos al final del artículo, todavía hay una brecha muy importante que debe cubrirse. Abra su IDE e intente generar el método equals() . Verá que no hay opción para generar solo el método equals() sin el método hashCode() . Debido a algunos tipos de colección en el lenguaje de programación Java, es una muy buena práctica no separar estos dos métodos entre sí. También se dice en la documentación oficial de Java que generalmente es necesario anular el método hashCode() cada vez que se anula el método equals() . Me gustaría dar la información correspondiente sobre el método hash y hashCode() en otro artículo mío.

Además, a veces es posible que no necesitemos apegarnos a las propiedades del método equals() para algunos casos excepcionales, como las clases de entidad de JPA. También merece su propio artículo.

En conclusión

Todos los puntos importantes para implementar el método equals() correcto se tratan en este artículo. También te recomiendo que escribas las pruebas unitarias correspondientes para probar la validación de las propiedades del método equals() . No olvides mirar los otros recursos que se pueden encontrar en la web.

En primer lugar, parece un tema muy fácil y primitivo ya que parte de los fundamentos del diseño orientado a objetos. Sin embargo, como se ve en los detalles, es muy importante no omitir estos conceptos básicos y comprenderlos completamente.