CodeGym /Cursos /JAVA 25 SELF /equals, hashCode, toString: geração automática

equals, hashCode, toString: geração automática

JAVA 25 SELF
Nível 22, Lição 2
Disponível

1. Geração automática de equals, hashCode, toString

Para que servem esses métodos?

Trabalhando com objetos em Java, você rapidamente se depara com as mesmas tarefas. Às vezes é preciso verificar se dois objetos são iguais. Por exemplo, entender se ele já está em uma coleção como Set ou Map. Em outros casos, o objeto é usado como chave em uma HashMap, e aí não dá para ficar sem regras especiais de comparação. E quase sempre queremos imprimir um objeto no log ou na tela de forma que a saída não seja apenas um amontoado de caracteres como MyClass@7b23ec81, e sim algo mais significativo.

Para esses casos, toda classe em Java tem três métodos especiais:

  • equals(Object o) é responsável por verificar a igualdade.
  • hashCode() dá ao objeto uma “impressão digital” numérica, necessária para coleções como tabelas de hash.
  • toString() retorna uma representação de string conveniente do objeto, o que facilita muito a depuração e a impressão.

Por que isso é trabalhoso em classes comuns?

Em classes comuns, esses métodos precisam ser escritos manualmente. E é aí que começam o tédio e a dor de cabeça. Sai um monte de código repetitivo que só entulha a classe. É muito fácil errar: esquecer de comparar um campo, calcular o hashCode de forma incorreta e depois caçar bugs misteriosos. E se a classe ganhar um novo campo — você terá que mexer novamente em todos esses métodos e reescrever tudo.

Exemplo de classe comum


public class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int x() { return x; }
    public int y() { return y; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Point point = (Point) o;
        return x == point.x && y == point.y;
    }

    @Override
    public int hashCode() {
        return 31 * x + y;
    }

    @Override
    public String toString() {
        return "Point[x=" + x + ", y=" + y + "]";
    }
}

Parece familiar? Sim, e isso é só para dois campos! E se fossem vinte?

Como o record faz isso

A classe record faz tudo isso por você. Basta declarar:


public record Point(int x, int y) { }

E o Java gera automaticamente:

  • Construtor
  • Getters (x(), y())
  • equals, hashCode, toString

Métodos gerados automaticamente

  • equals compara todos os componentes do record por valor.
  • hashCode é calculado com base em todos os componentes.
  • toString retorna uma string no formato Point[x=1, y=2].

Vamos ver na prática!


public record Point(int x, int y) {}

public class Demo {
    public static void main(String[] args) {
        Point p1 = new Point(1, 2);
        Point p2 = new Point(1, 2);

        System.out.println(p1.equals(p2)); // true
        System.out.println(p1.hashCode() == p2.hashCode()); // true
        System.out.println(p1); // Point[x=1, y=2]
    }
}

Saída:

true
true
Point[x=1, y=2]

Tudo funciona como esperado, sem uma única linha de código a mais!

2. Por que isso é importante: coleções, depuração e segurança

Funcionamento correto em coleções

Imagine que você usa objetos como chaves em uma HashMap ou elementos em um HashSet. Se equals e hashCode forem implementados de forma incorreta — as coleções se comportarão de modo estranho: não encontrarão um elemento que você acabou de adicionar ou, ao contrário, considerarão dois objetos diferentes como iguais.

Com classes record você pode ter certeza: a comparação e o hash sempre consideram todos os componentes do record (na ordem em que são declarados).

Exemplo: usando um record como chave


import java.util.HashMap;
import java.util.Map;

public class Demo {
    public static void main(String[] args) {
        record Point(int x, int y) {}

        Map<Point, String> map = new HashMap<>();
        Point p1 = new Point(3, 4);
        map.put(p1, "Hello!");

        Point p2 = new Point(3, 4);
        System.out.println(map.get(p2)); // "Hello!" — funciona!
    }
}

Perceba: p1 e p2 são objetos diferentes (referências diferentes), mas contêm os mesmos valores de campos, portanto são considerados iguais. E mais detalhes sobre Map e HashMap você verá no nível 26 :P

Conveniência na depuração e no registro em log

Em vez do desanimador Point@1a2b3c4d (como costuma acontecer por padrão em classes comuns), um record é impresso de forma bonita e informativa:

Point[x=3, y=4]

Isso economiza muito tempo na depuração e no registro em log.

3. Como equals, hashCode e toString funcionam dentro de um record

Método equals

Um record implementa equals de modo que dois objetos sejam considerados iguais se:

  • Forem do mesmo tipo (mesma classe record)
  • Todos os seus componentes forem iguais (== para primitivos, equals() para objetos)

Exemplo de comparação


Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
Point p3 = new Point(1, 3);

System.out.println(p1.equals(p2)); // true
System.out.println(p1.equals(p3)); // false

Método hashCode

O hash é calculado a partir de todos os componentes do record, geralmente usando o método padrão Objects.hash(...).


System.out.println(p1.hashCode()); // Por exemplo, 994
System.out.println(p2.hashCode()); // Também 994
System.out.println(p3.hashCode()); // Outro número

Método toString

A representação em string é sempre no formato:

ClassName[field1=value1, field2=value2, ...]

System.out.println(p1); // Point[x=1, y=2]

Sobrescrevendo equals, hashCode, toString: quando e como?

Às vezes (raramente, mas acontece) é preciso alterar o comportamento padrão desses métodos. Por exemplo, você quer que toString retorne uma string em outro formato, ou que a comparação aconteça apenas por parte dos campos.

Atenção: se você sobrescrever equals/hashCode, faça isso com muita consciência! Quebrar o “contrato” deles pode levar a bugs difíceis de rastrear.

Como sobrescrever um método

Basta declarar seu próprio método dentro do corpo do record:


public record Point(int x, int y) {
    @Override
    public String toString() {
        return "(" + x + "; " + y + ")";
    }
}

Point p = new Point(3, 5);
System.out.println(p); // (3; 5)

É possível sobrescrever equals/hashCode?

Sim, mas é fortemente não recomendado se você não tem certeza do que está fazendo. Por exemplo, se você quer que a comparação considere apenas o campo x (o que já é estranho):


public record Point(int x, int y) {
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point other)) return false;
        return x == other.x;
    }

    @Override
    public int hashCode() {
        return Integer.hashCode(x);
    }
}

Point p1 = new Point(1, 2);
Point p2 = new Point(1, 999);

System.out.println(p1.equals(p2)); // true (!)

Mas tenha cuidado: se você sobrescrever equals, sempre sobrescreva também hashCode — caso contrário, as coleções funcionarão de maneira incorreta.

Boas práticas

  • Se você não sabe exatamente por que sobrescrever — não sobrescreva!
  • Para toString — pode definir seu próprio formato, se quiser.
  • Para equals/hashCode — somente se houver um motivo forte e você entender as consequências.

5. Prática: comparar objetos e usar records em coleções

Exemplo: comparação de dois records


public record User(String name, int age) {}

public class Demo {
    public static void main(String[] args) {
        User u1 = new User("Alice", 20);
        User u2 = new User("Alice", 20);
        User u3 = new User("Bob", 25);

        System.out.println(u1.equals(u2)); // true
        System.out.println(u1.equals(u3)); // false

        System.out.println(u1.hashCode() == u2.hashCode()); // true
        System.out.println(u1); // User[name=Alice, age=20]
    }
}

Exemplo: usando um record como chave em uma HashMap

Vamos supor que temos um aplicativo onde armazenamos a quantidade de visitas de usuários pelo nome e idade (vai que no clube existam dois “Ivan, 20 anos”).


import java.util.HashMap;
import java.util.Map;

public class Demo {
    public static void main(String[] args) {
        record User(String name, int age) {}

        Map<User, Integer> visits = new HashMap<>();
        User ivan20 = new User("Ivan", 20);
        User ivan22 = new User("Ivan", 22);

        visits.put(ivan20, 5);
        visits.put(ivan22, 2);

        // Vamos verificar se a busca por valor funciona corretamente
        System.out.println(visits.get(new User("Ivan", 20))); // 5
        System.out.println(visits.get(new User("Ivan", 22))); // 2
    }
}

Se equals e hashCode não estivessem implementados corretamente, a busca não funcionaria. E mais detalhes sobre Map e HashMap você verá nas aulas do nível 26 :P

6. Erros comuns ao trabalhar com equals, hashCode e toString em records

Erro nº 1: Esperar que os campos possam ser alterados após a criação.
Os campos de um record são sempre final, e a comparação é feita pelos valores definidos no construtor. Se, por algum artifício, você “mudar” o estado interno (por exemplo, via um objeto mutável dentro do campo), a comparação e o hash podem se tornar incorretos.

Erro nº 2: Sobrescrever equals e esquecer do hashCode.
Se você sobrescrever um desses métodos — sempre sobrescreva o outro! Caso contrário, as coleções (HashSet, HashMap) se comportarão de forma imprevisível.

Erro nº 3: Esperar que toString tenha outro formato.
Se você precisa de um formato diferente de string — simplesmente sobrescreva toString. Por padrão, o formato é sempre ClassName[field1=value1, field2=value2].

Erro nº 4: Usar record para classes complexas com campos mutáveis.
Os campos de um record devem ser imutáveis. Se, como campo, você usa por exemplo um ArrayList, e alguém altera seu conteúdo — a comparação e o hash code podem “quebrar”. Para records, prefira usar apenas tipos imutáveis.

Erro nº 5: Usar record para classes cujo comportamento não é de value object.
Record não é “uma classe pequena com sintaxe curta”. É um value object, feito para armazenar um conjunto de valores. Se você tem lógica complexa, estado mutável ou necessidade de herança — use uma classe comum.

1
Tarefa
JAVA 25 SELF, nível 22, lição 2
Bloqueado
Detalhes do livro no relatório da biblioteca 📚
Detalhes do livro no relatório da biblioteca 📚
1
Tarefa
JAVA 25 SELF, nível 22, lição 2
Bloqueado
Identificação de localização no mapa 📍
Identificação de localização no mapa 📍
1
Tarefa
JAVA 25 SELF, nível 22, lição 2
Bloqueado
Apresentação formatada do perfil do usuário 🧑‍💻
Apresentação formatada do perfil do usuário 🧑‍💻
1
Tarefa
JAVA 25 SELF, nível 22, lição 2
Bloqueado
Identificação de produtos pelo nome na loja 🏷️
Identificação de produtos pelo nome na loja 🏷️
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION