Desafio Java

Igualdade com herança e campo mutável

Enunciado

Crie uma hierarquia com uma classe base Animal e uma subclasse Cachorro. A classe Animal tem um campo nome (String) e a subclasse adiciona raca (String). Implemente equals e hashCode de forma que um Cachorro só seja igual a outro Cachorro com mesmo nome e raça, e nunca igual a um Animal que não seja Cachorro. Além disso, crie um HashSet de Animal e demonstre que a igualdade funciona corretamente. Depois, torne o campo nome mutável e mostre o problema de usá-lo como chave em um HashMap após a mutação.

Requisitos

  • Animal deve ter equals e hashCode baseados em nome.
  • Cachorro deve sobrescrever equals e hashCode incluindo raca e usando getClass() para evitar igualdade entre subclasses diferentes.
  • O programa deve mostrar que um Cachorro não é igual a um Animal genérico com mesmo nome.
  • O programa deve demonstrar que mutar um campo usado no hashCode quebra a busca em HashMap.

Código inicial

import java.util.*;

class Animal {
    protected String nome;

    Animal(String nome) {
        this.nome = nome;
    }

    // TODO: equals e hashCode
}

class Cachorro extends Animal {
    private String raca;

    Cachorro(String nome, String raca) {
        super(nome);
        this.raca = raca;
    }

    // TODO: equals e hashCode
}

public class Main {
    public static void main(String[] args) {
        // Testes
    }
}

Saída esperada

Cachorro igual a Animal? false
Cachorro igual a Cachorro? true
Tamanho do HashSet: 1
Após mutação, valor recuperado: null
Ver dica

Use getClass() != o.getClass() para garantir que apenas objetos da mesma classe sejam iguais. Para o problema da mutação, insira a chave no mapa, modifique o campo e tente recuperar.

Mostrar solução
import java.util.*;

class Animal {
    protected String nome;

    Animal(String nome) {
        this.nome = nome;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Animal animal = (Animal) o;
        return Objects.equals(nome, animal.nome);
    }

    @Override
    public int hashCode() {
        return Objects.hash(nome);
    }
}

class Cachorro extends Animal {
    private String raca;

    Cachorro(String nome, String raca) {
        super(nome);
        this.raca = raca;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        if (!super.equals(o)) return false;
        Cachorro cachorro = (Cachorro) o;
        return Objects.equals(raca, cachorro.raca);
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), raca);
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Animal("Rex");
        Cachorro c1 = new Cachorro("Rex", "Labrador");
        Cachorro c2 = new Cachorro("Rex", "Labrador");

        System.out.println("Cachorro igual a Animal? " + c1.equals(animal));
        System.out.println("Cachorro igual a Cachorro? " + c1.equals(c2));

        Set<Animal> set = new HashSet<>();
        set.add(c1);
        set.add(c2);
        System.out.println("Tamanho do HashSet: " + set.size());

        // Demonstração do problema com campo mutável
        Map<Animal, String> mapa = new HashMap<>();
        Animal chave = new Animal("Toby");
        mapa.put(chave, "Valor");
        chave.nome = "NovoNome"; // mutação
        System.out.println("Após mutação, valor recuperado: " + mapa.get(chave));
    }
}

Passo a passo

  1. Animal define equals e hashCode baseados apenas em nome.
  2. Cachorro sobrescreve equals chamando super.equals(o) para comparar nome e depois compara raca.
  3. Usamos getClass() != o.getClass() em ambos para garantir que um Cachorro não seja igual a um Animal genérico, mesmo com mesmo nome.
  4. hashCode de Cachorro combina o hash da superclasse com o hash de raca.
  5. No main, criamos um Animal e dois Cachorro iguais. O HashSet armazena apenas um Cachorro porque equals e hashCode são consistentes.
  6. Depois, inserimos um Animal em um HashMap, mutamos seu nome e tentamos recuperar. O get retorna null porque o hash mudou e o mapa não encontra mais a chave no balde original.

Por que funciona

A implementação segue o contrato: objetos iguais têm o mesmo hash, e a igualdade entre classes diferentes é evitada com getClass(). O HashSet funciona corretamente. Já o exemplo com mutação mostra que alterar um campo usado no hashCode quebra a busca, pois o objeto passa a ter um hash diferente do que foi usado na inserção.

Erros comuns

  • Usar instanceof em vez de getClass(): animal instanceof Cachorro pode permitir igualdade assimétrica se Cachorro também sobrescrever equals. Use getClass() para garantir simetria.
  • Esquecer de chamar super.equals: em Cachorro, se não chamar super.equals(o), a comparação de nome não é feita.
  • Mutar campos usados em hashCode: como visto, isso quebra o HashMap. Evite usar campos mutáveis como chave ou torne a classe imutável.

Outra forma de resolver

Usar record para classes imutáveis, mas herança não é permitida em records. Para hierarquias, pode-se usar composição em vez de herança, evitando os problemas de igualdade entre subclasses.

Saída esperada

Cachorro igual a Animal? false
Cachorro igual a Cachorro? true
Tamanho do HashSet: 1
Após mutação, valor recuperado: null