Métodos de objetos de Java: igual (Objeto)

M

 

Introducción

Este artículo es una continuación de una serie de artículos que describen los métodos a menudo olvidados de la clase Object base del lenguaje Java. Los siguientes son los métodos del objeto Java base que están presentes en todos los objetos Java debido a la herencia implícita de Object.

  • Encadenar
  • getClass
  • es igual (estás aquí)
  • código hash
  • clon
  • finalizar
  • esperar y notificar

El enfoque de este artículo es el equals(Object) método que se utiliza para probar la igualdad entre objetos y le da al desarrollador la capacidad de definir una prueba significativa de equivalencia lógica.

== vs es igual a (Objeto)

Como habrás adivinado, equals(Object) El método se utiliza para probar la igualdad entre tipos de referencia (objetos) en Java. Vale, tiene sentido, pero también podrías estar pensando “¿Por qué no puedo usar ==? “La respuesta a esta pregunta es que cuando se trata de tipos de referencia, == El operador solo es verdadero cuando se comparan dos referencias al mismo objeto instanciado en la memoria. Por otro lado el equals(Object) se puede anular para implementar la noción de equivalencia lógica en lugar de la mera equivalencia de instancia.

Creo que un ejemplo describiría mejor esta diferencia entre usar el == verso el equals(Object) método en Strings.

public class Main {
    public static void main(String[] args) {
        String myName = "Adam";
        String myName2 = myName; // references myName
        String myName3 = new String("Adam"); // new instance but same content

        if (myName == myName2)
            System.out.println("Instance equivalence: " + myName + " & " + myName2);

        if (myName.equals(myName2))
            System.out.println("Logical equivalence: " + myName + " & " + myName2);

        if (myName == myName3)
            System.out.println("Instance equivalence: " + myName + " & " + myName3);

        if (myName.equals(myName3))
            System.out.println("Logical equivalence: " + myName + " & " + myName3);
    }
}

Salida:

Instance equivalence: Adam & Adam
Logical equivalence: Adam & Adam
Logical equivalence: Adam & Adam

En el ejemplo anterior, creé y comparé tres variables de cadena: myName, myName2 que es una copia de la referencia a myNamey myName3 que es una instancia totalmente nueva pero con el mismo contenido. Primero muestro que el == el operador identifica myName y myName2 como una instancia equivalente, lo que esperaría porque myName2 es solo una copia de la referencia. Debido al hecho de que myName y myName2 son referencias de instancia idénticas, se deduce que tienen que ser lógicamente equivalentes.

Las dos últimas comparaciones realmente demuestran la diferencia entre usar == y equals(Object). La comparación de instancias usando == demuestra que son instancias diferentes con sus propias ubicaciones de memoria únicas, mientras que la comparación lógica utilizando equals(Object) muestra que contienen exactamente el mismo contenido.

Bucear en iguales (Objeto)

Ok, ahora sabemos la diferencia entre == y equals(Object), pero ¿y si te dijera que la implementación base de la clase Object en realidad produce el mismo resultado que el == ¿operador?

Qué…!? Lo sé … eso parece extraño, pero bueno, los desarrolladores de Java tuvieron que empezar por alguna parte. Permítanme decirlo de nuevo, por defecto el equals(Object) El método que hereda en sus clases personalizadas simplemente prueba, por ejemplo, la igualdad. Depende de nosotros, como desarrolladores, determinar si esto es apropiado o no, es decir, determinar si existe una noción de equivalencia lógica que se requiere para nuestra clase.

De nuevo, déjame usar el Person clase que presenté anteriormente en esta serie para más demostración.

public class Person {
    private String firstName;
    private String lastName;
    private LocalDate dob;

    public Person(String firstName, String lastName, LocalDate dob) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
    }

    // omitting getters and setters for brevity

    @Override
    public String toString() {
        return "<Person: firstName=" + firstName + ", lastName=" + lastName + ", dob=" + dob + ">";
    }
}

Permítanme usar de nuevo un programa simple envuelto en un Main clase que demuestra la igualdad de instancia idéntica y la igualdad lógica anulando equals(Object).

import java.time.LocalDate;

public class Main {
    public static void main(String[] args) {
        Person me = new Person("Adam", "McQuistan", LocalDate.parse("1987-09-23"));
        Person me2 = new Person("Adam", "McQuistan", LocalDate.parse("1987-09-23"));

        if (me != me2)
            System.out.println("Not instance equivalent");

        if (!me.equals(me2))
            System.out.println("Not logically equivalent");
    }
}

Salida:

Not instance equivalent
Not logically equivalent

Como puede ver las instancias de dos personas me y me2 no son ni lógica ni equivalentes de instancia fuera de la caja, a pesar de que uno podría concebir razonablemente que me y me2 representan lo mismo en función del contenido.

Aquí es donde se vuelve importante anular la implementación predeterminada y proporcionar una que tenga sentido para la clase que se está definiendo. Sin embargo, de acuerdo con los documentos oficiales de Java, hay algunas reglas que deben seguirse al hacerlo para evitar problemas con algunas dependencias de implementación importantes del lenguaje.

Las reglas descritas en el es igual a los documentos de Java para instancias de objetos dadas x, yy z son como sigue:

  • reflexivo: x.equals(x) debe ser verdadero para todas las instancias de referencia no nulas de x
  • simétrico: x.equals(y) y y.equals(x) debe ser verdadero para todas las instancias de referencia no nulas de x y y
  • transitivo: si x.equals(y) y y.equals(z) entonces x.equals(z) también debe ser cierto para instancias de referencia no nulas de x, yy z
  • consistencia: x.equals(y) siempre debe ser verdadero cuando ningún valor de miembro utilizado en la implementación de iguales haya cambiado en x y y instancias de referencia no nulas
  • sin igualdad nula: x.equals(null) nunca debe ser verdad
  • siempre anular hashCode() al anular equals()

Desembalaje de las reglas de anular iguales (objeto)

A. Reflexivo: x.equals (x)

Para mí, esto es lo más fácil de entender. Además de la implementación predeterminada del equals(Object) El método lo garantiza, pero en aras de la integridad, proporcionaré una implementación de ejemplo a continuación que sigue esta regla:

class Person {
    // omitting for brevity

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        return false;
    }
}

B. Simétrico: x.equals (y) e y.equals (x)

Este puede parecer intuitivo a primera vista, pero en realidad es bastante fácil cometer un error y violar esta regla. De hecho, la razón principal por la que esto se viola a menudo es en los casos de herencia, que resulta ser algo muy popular en Java.

Antes de dar un ejemplo, permítame actualizar el equals(Object) método para tener en cuenta el nuevo requisito más obvio, que es el hecho de que la prueba de equivalencia debe implementar una prueba lógica además de la prueba de igualdad de instancia.

Para implementar una prueba lógica, querré comparar los campos que contienen estados entre dos instancias de la clase de personas, descritas como x y y. Además, también debería verificar para asegurarme de que las dos instancias sean del mismo tipo de instancia, así:

class Person {
    // omitting for brevity

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Person)) {
            return false;
        }
        Person p = (Person)o;
        return firstName.equals(p.firstName)
                && lastName.equals(p.lastName)
                && dob.equals(p.dob);
    }
}

Ok, debería ser evidente que Person ahora tiene una mucho mas robusta equals(Object) implementación. Ahora déjeme dar un ejemplo de cómo la herencia puede causar una violación de la simetría. A continuación se muestra una clase aparentemente inofensiva, llamada Employee, que hereda de Person.

import java.time.LocalDate;

public class Employee extends Person {

    private String department;

    public Employee(String firstName, String lastName, LocalDate dob, String department) {
        super(firstName, lastName, dob);
        this.department = department;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (!(o instanceof Employee)) {
            return false;
        }
        Employee p = (Employee)o;
        return super.equals(o) && department.equals(p.department);

    }
}

Con suerte, puede notar que estos no deben tratarse como instancias iguales, pero es posible que se sorprenda con lo que estoy a punto de mostrarle.

import java.time.LocalDate;

public class Main {
    public static void main(String[] args) {
        Person billy = new Person("Billy", "Bob", LocalDate.parse("2016-09-09"));
        MinorPerson billyMinor = new MinorPerson(
                billy.getFirstName(),
                billy.getLastName(),
                billy.getDob());

        System.out.println("billy.equals(billyMinor): " + billy.equals(billyMinor));
        System.out.println("billyMinor.equals(billy): " + billyMinor.equals(billy));
    }
}

Salida:

billy.equals(billyEmployee): true
billyEmployee.equals(billy): false

¡Ups! Claramente una violación de la simetría, billy es igual a billyEmployee pero lo contrario no es cierto. ¿Entonces qué hago? Bueno, podría hacer algo como lo siguiente, dado que escribí el código y sé qué hereda qué, luego modifiqué el Employee equals(Object) método así:

import java.time.LocalDate;

public class Employee extends Person {

    private String department;

    public Employee(String firstName, String lastName, LocalDate dob, String department) {
        super(firstName, lastName, dob);
        this.department = department;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (instanceof Person && !(o instanceof Employee)) {
            return super.equals(o);
        }

        if (o instanceof Employee) {
            Employee p = (Employee)o;
            return super.equals(o) && department.equals(p.department);
        }

        return false;
    }
}

Salida:

billy.equals(billyEmployee): true
billyEmployee.equals(billy): true

¡Sí, tengo simetría! ¿Pero estoy realmente bien? Fíjate aquí cómo voy a salir de mi camino para hacer Employee ahora conforme … esto debería estar enviando una bandera roja que volverá a morderme más tarde como demuestro en la siguiente sección.

C. Transitividad: si x.equals (y) e y.equals (z) entonces x.equals (z)

Hasta ahora me he asegurado de que mi Person y Employee las clases tienen equals(Object) métodos que son tanto reflexivos como simétricos, por lo que debo verificar que también se esté siguiendo la transitividad. Lo haré a continuación.

import java.time.LocalDate;

public class Main {
    public static void main(String[] args) {
        Person billy = new Person("Billy", "Bob", LocalDate.parse("2016-09-09"));
        Employee billyEngineer = new Employee(
                billy.getFirstName(),
                billy.getLastName(),
                billy.getDob(),
                "Engineering");
        Employee billyAccountant = new Employee("Billy", "Bob", LocalDate.parse("2016-09-09"), "Accounting");

        System.out.println("billyEngineer.equals(billy): " + billyEngineer.equals(billy));
        System.out.println("billy.equals(billyAccountant): " + billy.equals(billyAccountant));
        System.out.println("billyAccountant.equals(billyEngineer): " + billyAccountant.equals(billyEngineer));
    }
}

Salida:

billyEngineer.equals(billy): true
billy.equals(billyAccountant): true
billyAccountant.equals(billyEngineer): false

¡Maldito! Estuve en un buen camino allí durante un tiempo. ¿Que pasó? Bueno, resulta que en la herencia clásica dentro del lenguaje Java, no puede agregar un miembro de clase de identificación a una subclase y aún así esperar poder anular equals(Object) sin violar la simetría ni la transitividad. La mejor alternativa que he encontrado es utilizar patrones de composición en lugar de herencia. Esto efectivamente rompe la rígida jerarquía de herencia entre las clases, así:

import java.time.LocalDate;

public class GoodEmployee {

    private Person person;
    private String department;

    public GoodEmployee(String firstName, String lastName, LocalDate dob, String department) {
        person = new Person(firstName, lastName, dob);
        this.department = department;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }

        if (!(o instanceof Employee)) {
            return false;
        }

        GoodEmployee p = (GoodEmployee)o;
        return person.equals(o) && department.equals(p.department);
    }
}

D. Consistencia: x.equals (y) siempre que nada cambie

Este es realmente muy fácil de comprender. Básicamente, si dos objetos son iguales, solo permanecerán iguales mientras ninguno de ellos cambie. Aunque esto es fácil de entender, se debe tener cuidado para garantizar que los valores no cambien si pudiera haber consecuencias negativas como resultado de tales cambios.

La mejor forma de asegurarse de que las cosas no cambien en una clase es hacerla inmutable proporcionando solo una forma de asignar valores. Generalmente, esta única forma de asignación debe ser a través de un constructor durante la instanciación. También declarando campos de clase final puede ayudar con esto.

A continuación se muestra un ejemplo de Person clase definida como una clase inmutable. En este caso, dos objetos que inicialmente son iguales siempre serán iguales porque no se puede cambiar su estado una vez creados.

import java.time.LocalDate;

public class Person {
    private final String firstName;
    private final String lastName;
    private final LocalDate dob;

    public Person(String firstName, String lastName, LocalDate dob) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dob = dob;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public LocalDate getDob() {
        return dob;
    }

    @Override
    public String toString() {
        Class c = getClass();
        return "<" + c.getSimpleName() + ": firstName=" + firstName + ", lastName=" + lastName + ", dob=" + dob + ">";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof Person)) {
            return false;
        }
        Person p = (Person)o;
        return firstName.equals(p.firstName)
                && lastName.equals(p.lastName)
                && dob.equals(p.dob);
    }
}

E. Sin igualdad nula: x.equals (nulo)

A veces verá que esto se aplica a través de una verificación directa de la Object ejemplo o siendo igual a null, pero en el ejemplo anterior esto se verifica implícitamente usando el !(o instanceof Person) debido al hecho de que el instanceof El comando siempre devolverá falso si el operando izquierdo es nulo.

F. Siempre anular hashCode() al anular equals(Object)

Debido a la naturaleza de varios detalles de implementación en otras áreas del lenguaje Java, como el marco de colecciones, es imperativo que si equals(Object) se anula entonces hashCode() debe anularse también. Dado que el próximo artículo de esta serie cubrirá específicamente los detalles de la implementación de su propio hasCode() método No cubriré este requisito con más detalle aquí, excepto para decir que dos instancias que exhiben igualdad a través de la equals(Object) El método debe producir códigos hash idénticos a través de hashCode().

Conclusión

Este artículo describe el significado y uso de la equals(Object) método junto con por qué puede ser importante para sus programas tener una noción de igualdad lógica que difiera de la igualdad de identidad (instancia).

Como siempre, gracias por leer y no dude en comentar o criticar a continuación.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad