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.
GO TO FULL VERSION