Introducción
Contenido
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 myName
y 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
, y
y z
son como sigue:
- reflexivo:
x.equals(x)
debe ser verdadero para todas las instancias de referencia no nulas dex
- simétrico:
x.equals(y)
yy.equals(x)
debe ser verdadero para todas las instancias de referencia no nulas dex
yy
- transitivo: si
x.equals(y)
yy.equals(z)
entoncesx.equals(z)
también debe ser cierto para instancias de referencia no nulas dex
,y
yz
- consistencia:
x.equals(y)
siempre debe ser verdadero cuando ningún valor de miembro utilizado en la implementación de iguales haya cambiado enx
yy
instancias de referencia no nulas - sin igualdad nula:
x.equals(null)
nunca debe ser verdad - siempre anular
hashCode()
al anularequals()
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.
Te puede interesar:El diseño del generador de patrones en Java