Anular el método java equals () - ¿no funciona?

150

Me encontré con un problema interesante (y muy frustrante) con el equals()método hoy que causó el bloqueo de lo que pensé que era una clase bien probada y causó un error que me llevó mucho tiempo rastrear.

Solo para completar, no estaba usando un IDE o depurador, solo un buen editor de texto antiguo y System.out. El tiempo era muy limitado y era un proyecto escolar.

De todos modos

Que estaba desarrollando un carrito de la compra básica que podría contener un ArrayListde Booklos objetos . Con el fin de poner en práctica los addBook(), removeBook()y hasBook()métodos de la Cesta, quería comprobar si el Bookya existía en el Cart. Entonces me voy

public boolean equals(Book b) {
    ... // More code here - null checks
    if (b.getID() == this.getID()) return true;
    else return false;
}

Todo funciona bien en las pruebas. Creo 6 objetos y los lleno con datos. Realiza muchas operaciones de adición, eliminación, has () en Carty todo funciona bien. Leí que puedes tener equals(TYPE var)oequals(Object o) { (CAST) var } asumir que, dado que funcionaba, no importaba demasiado.

Luego me encontré con un problema: necesitaba crear un Bookobjeto con solo el contenido IDdentro de la clase Libro. No se ingresarán otros datos. Básicamente lo siguiente:

public boolean hasBook(int i) {
    Book b = new Book(i);
    return hasBook(b);
}

public boolean hasBook(Book b) {
    // .. more code here
    return this.books.contains(b);
}

De repente, el equals(Book b)método ya no funciona. Esto tomó MUCHO tiempo para rastrear sin un buen depurador y suponiendo que la Cartclase se probó y corrigió correctamente. Después de cambiar el equals()método a lo siguiente:

public boolean equals(Object o) {
    Book b = (Book) o;
    ... // The rest goes here   
}

Todo comenzó a funcionar nuevamente. ¿Hay alguna razón por la cual el método decidió no tomar el parámetro Libro a pesar de que claramente era un Bookobjeto? La única diferencia parecía ser que se instanciaba dentro de la misma clase y solo se llenaba con un miembro de datos. Estoy muy muy confundido Por favor, arrojar algo de luz?

Josh Smeaton
fuente
1
Soy consciente de que violé el 'Contrato' con respecto a la anulación de los métodos de igualdad al ser reflexivo; sin embargo, necesitaba una forma rápida de verificar si el objeto existía en ArrayList sin usar genéricos.
Josh Smeaton el
1
Esta es una buena lección para aprender sobre Java e iguales
jjnguy

Respuestas:

329

En Java, el equals()método que se hereda Objectes:

public boolean equals(Object other);

En otras palabras, el parámetro debe ser de tipo Object. Esto se llama anulación ; su método public boolean equals(Book other)hace lo que se llama sobrecarga al equals()método.

Los ArrayListusos sobrescritos equals()métodos para comparar los contenidos (por ejemplo, para su contains()y equals()métodos), no sobrecargados queridos. En la mayoría de su código, llamar al que no anuló correctamente Objectlos iguales estuvo bien, pero no es compatible con ArrayList.

Por lo tanto, no anular el método correctamente puede causar problemas.

Anulo es igual a lo siguiente cada vez:

@Override
public boolean equals(Object other){
    if (other == null) return false;
    if (other == this) return true;
    if (!(other instanceof MyClass)) return false;
    MyClass otherMyClass = (MyClass)other;
    ...test other properties here...
}

El uso de la @Overrideanotación puede ayudar mucho con errores tontos.

Úselo siempre que piense que está anulando el método de una superclase o interfaz. De esa manera, si lo hace de manera incorrecta, obtendrá un error de compilación.

jjnguy
fuente
31
Este es un buen argumento a favor de la anotación @Override ... si el OP hubiera utilizado @Override, su compilador le habría dicho que en realidad no estaba anulando un método de clase padre ...
Cowan
1
Nunca conocí el @Override, ¡gracias por eso! También me gustaría agregar que anular hashCode () realmente debería haberse hecho y puede haber detectado el error antes.
Josh Smeaton el
55
Algunos IDEs (por ejemplo, Eclipse) pueden incluso autogenerar los métodos equals () y hashcode () en función de las variables del miembro de la clase.
sk.
1
if (!(other instanceof MyClass))return false;devuelve falsesi MyClassextiende la otra clase. Pero no volvería falsesi la otra clase se extendiera MyClass. ¿No debería equalser menos contradictorio?
Robert
19
Cuando se usa instanceof, el nullcheck anterior es redundante.
Mateusz Dymczyk
108

Si usa eclipse solo vaya al menú superior

Fuente -> Generar equals () y hashCode ()

Fred
fuente
¡Estoy de acuerdo! Este nunca lo había conocido antes y generarlo lo hace menos propenso a errores
Boy
Igual que aquí. Gracias Fred!
Anila
16
En IntelliJ encontrará esto en Código → Generar ... o control + N. :)
plegado a la derecha
En Netbeans, vaya a la barra de menú> Fuente (o haga clic derecho)> Insertar código (o Ctrl-I), y haga clic en Generar iguales () ...
Solomon
11

Ligeramente fuera de tema a su pregunta, pero probablemente vale la pena mencionar de todos modos:

Commons Lang tiene algunos métodos excelentes que puedes usar para anular equals y hashcode. Consulte EqualsBuilder.reflectionEquals (...) y HashCodeBuilder.reflectionHashCode (...) . Me ahorró mucho dolor de cabeza en el pasado, aunque, por supuesto, si solo quiere hacer "iguales" en la identificación, puede que no se ajuste a sus circunstancias.

También estoy de acuerdo en que debe usar la @Overrideanotación siempre que anule iguales (o cualquier otro método).


fuente
44
Si eres un usuario de eclipse, también puedes ir right click -> source -> generate hashCode() and equals(),
tunaranch
1
¿Tengo razón en que este método se ejecuta en tiempo de ejecución? ¿No tendremos problemas de rendimiento en caso de que atraviesemos una gran colección con elementos que verifiquen su igualdad con algún otro elemento debido a la reflexión?
Gaket
4

Otra solución rápida que ahorra código repetitivo es la anotación Lombok EqualsAndHashCode . Es fácil, elegante y personalizable. Y no depende del IDE . Por ejemplo;

import lombok.EqualsAndHashCode;

@EqualsAndHashCode(of={"errorNumber","messageCode"}) // Will only use this fields to generate equals.
public class ErrorMessage{

    private long        errorNumber;
    private int         numberOfParameters;
    private Level       loggingLevel;
    private String      messageCode;

Vea las opciones disponibles para personalizar qué campos usar en los iguales. Lombok está disponible en maven . Simplemente agréguelo con el alcance proporcionado :

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.14.8</version>
    <scope>provided</scope>
</dependency>
borjab
fuente
1

en Android Studio es alt + insert ---> equals y hashCode

Ejemplo:

    @Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    Proveedor proveedor = (Proveedor) o;

    return getId() == proveedor.getId();

}

@Override
public int hashCode() {
    return getId();
}
David Hackro
fuente
1

Considerar:

Object obj = new Book();
obj.equals("hi");
// Oh noes! What happens now? Can't call it with a String that isn't a Book...
bcsb1001
fuente
1
@Elazar ¿Cómo es eso? objse declara como un Object. El punto de herencia es que luego puede asignar un Booka obj. Después de eso, a menos que sugiera que un Objectno debería ser comparable a una Stringvía equals(), este código debería ser perfectamente legal y de devolución false.
bcsb1001
Sugiero exactamente eso. Creo que es bastante ampliamente aceptado.
Elazar
0

la instanceOfdeclaración se usa a menudo en la implementación de iguales.

¡Esta es una trampa popular!

El problema es que el uso instanceOfviola la regla de simetría:

(object1.equals(object2) == true) si y solo si (object2.equals(object1))

si el primer igual es verdadero y object2 es una instancia de una subclase de la clase a la que pertenece obj1, ¡el segundo igual devolverá falso!

si la clase considerada a la que pertenece ob1 se declara como final, entonces este problema no puede surgir, pero en general, debe probar lo siguiente:

this.getClass() != otherObject.getClass(); si no, devuelve falso, de lo contrario, pruebe los campos para comparar la igualdad.

Nikel8000
fuente
3
Ver Bloch, Effective Java, Item 8, una gran sección que discute los problemas para anular el equals()método. Recomienda contra el uso getClass(). La razón principal es que hacerlo rompe el Principio de sustitución de Liskov para las subclases que no afectan la igualdad.
Stuart Marks
-1

recordId es propiedad del objeto

@Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Nai_record other = (Nai_record) obj;
        if (recordId == null) {
            if (other.recordId != null)
                return false;
        } else if (!recordId.equals(other.recordId))
            return false;
        return true;
    }
vootla561
fuente