Como escrever equals () em Java

Feb 22 2023
Ao final deste artigo, todas as informações necessárias serão abordadas para poder escrever o melhor método equals() na linguagem de programação Java. Os detalhes da igualdade serão tratados em termos de definição matemática e à maneira da linguagem de programação Java, a fim de abranger as informações fundamentais necessárias para usar a melhor prática do método equals().

Ao final deste artigo, todas as informações necessárias serão abordadas para poder escrever o melhor método equals() na linguagem de programação Java. Os detalhes da igualdade serão tratados em termos de definição matemática e à maneira da linguagem de programação Java, a fim de abranger as informações fundamentais necessárias para usar a melhor prática do método equals( ) .

Definição matemática de igualdade

Segundo a Wikipedia , a definição de igualdade matemática é

Em matemática, a igualdade é uma relação entre duas quantidades ou, mais geralmente, duas expressões matemáticas, afirmando que as quantidades têm o mesmo valor ou que as expressões representam o mesmo objeto matemático. A igualdade entre A e B é escrita A = B , e pronunciado A é igual a B .

Definição de Igualdade em Java

Na linguagem de programação Java, igualdade significa simplesmente ter o mesmo valor. Às vezes, significa literalmente ter o mesmo valor e, às vezes, significa ter a mesma estrutura em termos das propriedades do objeto.

Quando é necessário verificar a igualdade de dois valores de tipo primitivo, o sinal de igual é usado diretamente como no exemplo abaixo;

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 outro lado, às vezes é necessário verificar a igualdade de dois objetos. Em termos de objetos, existem duas abordagens diferentes que são usadas em diferentes etapas do processo de verificação da igualdade de dois objetos. Vamos entrar em detalhes.

Igualdade Referencial

Na linguagem de programação Java, os valores reais dos objetos são armazenados em algum lugar da memória e são apontados pelas referências de objetos que também são armazenadas em algum lugar da memória. Vejamos o trecho de código a seguir;

// 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);

Em tal cenário, “a” e “b” são na verdade duas referências que apontam para o mesmo objeto, então pode-se dizer diretamente que “a” é igual a “b” desde que a igualdade referencial seja obtida.

No entanto, o cenário oposto não é válido o tempo todo. Duas referências diferentes podem apontar para locais de memória diferentes, mas os objetos para os quais as referências apontam ainda podem ser iguais.

Na vida normal, às vezes as quantidades são comparadas. Por exemplo, se você tem duas maçãs e seu amigo tem duas maçãs, então o número de maçãs que você e seu amigo têm é igual.

Por outro lado, às vezes as coisas também podem ser comparadas em termos de suas propriedades ou estrutura. Vamos entrar em detalhes para este cenário.

Igualdade Estrutural

Igualdade estrutural é o tipo de qual os objetos são comparados de acordo com os estados de suas propriedades. Uma ferramenta mais sofisticada é necessária para verificar esse tipo de igualdade em vez de apenas o operador “==”. É o método equals(). Vem da classe Object que está no ponto mais alto da hierarquia de classes na linguagem de programação Java. Vamos verificar o próximo trecho 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);
    }
    
}

Na segunda linha começamos a verificar a identidade dos objetos. Se a referência de objeto o for nula ou ambas as referências de objeto apontarem para objetos que pertencem a classes diferentes, pode-se dizer diretamente que esses dois objetos não têm a mesma identidade e não são iguais. Se não for esse o caso, finalmente poderemos verificar as estruturas reais dos objetos.

Na terceira linha do método, lançamos a referência o para o tipo de classe correspondente, pois o método foi substituído pela classe Object e agora sabemos que ele pertence à classe Car.

Finalmente, na última linha do método, a estrutura de dois objetos é verificada e é nesta linha que a identidade é definida. Simplesmente, se os modelos das marcas e os anos do modelo forem os mesmos, esses dois objetos serão iguais. Em outras palavras, esta é a igualdade estrutural.

Aprendemos as propriedades da igualdade matemática na escola primária. Algumas dessas propriedades também devem ser válidas para o método equals() . Isso significa que essas propriedades devem ser adicionadas aos ingredientes do método equals() . Vamos ver essas propriedades em termos de igualdade matemática e em termos de igualdade em Java;

Propriedade reflexiva

Na definição matemática da propriedade reflexiva, para todos os números reais x ;

x = x

Um número é igual a si mesmo.

Em termos de igualdade Java, um objeto deve ser igual a si mesmo;

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);
    }

Propriedade Simétrica

Em termos de definição matemática da propriedade simétrica, para todos os números reais x e y ;

Se x = y , então y = x

A ordem da igualdade não importa.

Em termos de igualdade Java, se o objeto a é igual ao objeto b, então o objeto b tem que ser igual ao 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));

Propriedade Transitiva

Na definição matemática da propriedade transitiva, para todos os números reais x , y e z ;

se x = y e y = z , então x = z

Dois números iguais ao mesmo número são iguais entre si.

Em termos de igualdade Java, se o objeto a é igual ao objeto b e o objeto b é igual ao objeto c, então o objeto a tem que ser igual ao 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));

Propriedade consistente

Esta é uma propriedade diferente, pois não há um equivalente exato matematicamente. Para este, precisamos obter o mesmo resultado para execuções consecutivas do método equals().

Vejamos o trecho de código abaixo;

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 exemplo realmente não me satisfaz. Depois de uma extensa pesquisa, vou substituí-lo por um melhor. É dito claramente na documentação Java do método equals() que “múltiplas invocações de x.equals(y)retorno consistente trueou retorno consistente false”. Deve haver um exemplo sólido de violação para esta declaração.

Propriedade de parâmetro nulo

É assim que escolho chamar essa propriedade.

Observe o trecho de código abaixo;

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

Agora está claro quais propriedades uma implementação do método equals() deve ter para fornecer um cálculo correto da igualdade. Além disso, vimos os possíveis cenários de violação, então agora podemos evitá-los em prol da qualidade do código e da lógica de negócios.

Hoje, muitos IDEs fornecem mecanismos de geração para gerar as melhores implementações de método equals() possíveis . É uma boa ideia ficar com eles a maior parte do tempo ao invés de implementar o método equals() . Vejamos um exemplo do 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);
   }

Na primeira linha, verifica-se a igualdade referencial, pois duas referências de objeto podem apontar para o mesmo objeto.

Na segunda linha, a validação da propriedade de parâmetro nulo é garantida e também a mesma condição if atua como uma cláusula de guarda , controlando o tipo de classe dos objetos que são apontados por referências para evitar a violação da propriedade transitiva e da propriedade simétrica .

Na terceira linha, a referência do objeto é lançada no tipo de classe correspondente que é Car em nosso exemplo para alcançar as propriedades do objeto Car.

Finalmente, na quarta linha verifica-se a igualdade em termos da igualdade estrutural.

Existem outros modelos para geração do método equals() . Você também pode ir e verificá-los.

Se você quiser escrever a implementação por conta própria, lembre-se de que a implementação deve garantir a validade de todas as propriedades do método equals() .

Ao chegarmos ao final do artigo, ainda há uma lacuna muito importante que precisa ser preenchida. Abra seu IDE e tente gerar o método equals () . Você verá que não há opção para gerar apenas o método equals() sem o método hashCode() . Devido a alguns tipos de coleção na linguagem de programação Java, é uma boa prática não separar esses dois métodos um do outro. Também é dito na documentação oficial do Java que geralmente é necessário substituir o método hashCode() sempre que o método equals() for substituído. Gostaria de fornecer informações correspondentes sobre o método hashing e hashCode () em outro artigo meu.

Além disso, às vezes podemos não precisar nos ater às propriedades do método equals() para alguns casos excepcionais, como classes de entidade de JPA. Também merece seu próprio artigo.

Para concluir

Todos os pontos importantes para implementar o método equals() correto são abordados neste artigo. Também recomendo que você escreva os testes de unidade correspondentes para testar a validação das propriedades do método equals() . Não se esqueça de olhar para os outros recursos que podem ser encontrados na web.

Em primeiro lugar, parece um tópico muito fácil e primitivo, já que vem dos fundamentos do design orientado a objetos. No entanto, como se vê pelos detalhes, é muito importante não pular esses conceitos básicos e entender completamente.