Java A Classe Object
Em Herança você viu que toda classe, no fim da escada, herda de java.lang.Object. Isso quer dizer que todo objeto Java, de qualquer classe, já nasce com alguns métodos. Três deles você vai sobrescrever o tempo todo: toString, equals e hashCode. E os dois últimos têm uma regra que, quando quebrada, faz coleções como o HashSet darem respostas erradas sem nenhuma mensagem de erro. Esta aula fecha o grupo "Herança e Polimorfismo" explicando cada um.
- Quais métodos todo objeto herda de
Object. - O que significa o
Aluno@5d20e46dotoStringpadrão. - A diferença entre
==eequals, e como sobrescrever oequals. - O que é o
hashCode, o contrato entre ele e oequals, e o que acontece quando o contrato é quebrado. - Para que serve o
getClass().
A ideia: o "kit básico" de todo objeto
Quando você compra um celular, qualquer marca, ele vem com o básico: liga, desliga, mostra a hora. Cada fabricante pode caprichar mais em cada função, mas o básico está sempre lá. A classe Object é o kit básico de todo objeto Java. Os métodos que ele traz:
| Método | Para quê | Costuma ser sobrescrito? |
|---|---|---|
String toString() | O objeto como texto | Sim, quase sempre |
boolean equals(Object o) | "Este objeto é igual àquele?" | Sim, em classes de dados |
int hashCode() | Um número que resume o objeto, usado por HashSet e HashMap | Sim, sempre junto com o equals |
Class<?> getClass() | A classe do objeto | Não (é final) |
clone() | Copiar o objeto | Raramente; prefira um construtor de cópia |
wait(), notify(), notifyAll() | Coordenação entre threads | Não (veja synchronized) |
finalize() | Antigo; obsoleto (deprecated) desde o Java 9 | Não use |
toString(): o que é Aluno@5d20e46?
Uma classe sem toString() próprio usa o de Object. O resultado é aquele texto estranho que você já viu:
public class Main {
public static void main(String[] args) {
Aluno ana = new Aluno("Ana", 123);
System.out.println(ana);
System.out.println(ana.toString());
System.out.println(ana.getClass().getName());
System.out.println(ana.hashCode());
System.out.println(Integer.toHexString(ana.hashCode()));
}
}
class Aluno {
String nome;
int matricula;
Aluno(String nome, int matricula) {
this.nome = nome;
this.matricula = matricula;
}
}
$ java Main.java Aluno@5d20e46 « o println chama o toString() Aluno@5d20e46 « a mesma coisa, chamado à mão Aluno « o nome da classe 97652294 « o hashCode, em decimal 5d20e46 « o mesmo número, em hexadecimal
As três últimas linhas desmontam a primeira. O toString() de Object é, literalmente:
return getClass().getName() + "@" + Integer.toHexString(hashCode());
- Aluno
- O nome completo da classe (com o pacote, se houver:
br.com.ana.Aluno; veja Pacotes). - @
- Só um separador.
- 5d20e46
- O
hashCode()do objeto, escrito em hexadecimal (base 16, com os dígitos 0–9 e a–f).5d20e46em hexadecimal é97652294em decimal.
Não é um "endereço de memória", como muita gente diz: é um número de identificação que a JVM inventa para o objeto. Ele pode mudar de uma execução para outra e de uma máquina para outra; não tente usá-lo para nada. A solução é sobrescrever o toString() com algo útil, como você aprendeu em Sobrescrita:
@Override
public String toString() {
return nome + " (matrícula " + matricula + ")";
}
Missão: rode e veja o Livro@…. Depois sobrescreva toString() em Livro para que o println mostre Dom Casmurro, de Machado de Assis (título, vírgula, "de" e o autor).
public class Main {
public static void main(String[] args) {
Livro livro = new Livro("Dom Casmurro", "Machado de Assis");
System.out.println(livro);
}
}
class Livro {
String titulo;
String autor;
Livro(String titulo, String autor) {
this.titulo = titulo;
this.autor = autor;
}
}
public class Main {
public static void main(String[] args) {
Livro livro = new Livro("Dom Casmurro", "Machado de Assis");
System.out.println(livro);
}
}
class Livro {
String titulo;
String autor;
Livro(String titulo, String autor) {
this.titulo = titulo;
this.autor = autor;
}
@Override
public String toString() {
return titulo + ", de " + autor;
}
}
equals: igual ou o mesmo?
"Igual" tem dois sentidos. Duas notas de 10 reais são iguais (valem o mesmo), mas não são a mesma nota (cada uma tem o seu número de série). Em Java:
a == bpergunta: "aebsão o mesmo objeto?" (apontam para o mesmo lugar da memória?)a.equals(b)pergunta: "aebsão iguais?", e quem decide o que é "igual" é a classe.
O problema: o equals que vem de Object não sabe nada sobre alunos, e simplesmente faz ==. Veja:
public class Main {
public static void main(String[] args) {
Aluno a1 = new Aluno("Ana", 123);
Aluno a2 = new Aluno("Ana", 123);
Aluno a3 = a1;
System.out.println(a1 == a2);
System.out.println(a1.equals(a2));
System.out.println(a1 == a3);
System.out.println(a1.equals(a3));
String s1 = new String("Ana");
String s2 = new String("Ana");
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
}
class Aluno {
String nome;
int matricula;
Aluno(String nome, int matricula) {
this.nome = nome;
this.matricula = matricula;
}
}
$ java Main.java false « a1 == a2: dois objetos diferentes false « a1.equals(a2): o equals de Object é o mesmo que == true « a1 == a3: a3 aponta para o mesmo objeto que a1 true false « duas Strings diferentes na memória… true « …mas a String sobrescreveu o equals: compara o texto
As duas últimas linhas mostram a diferença: a classe String sobrescreveu o equals para comparar as letras (por isso, na aula de Strings, a regra é "compare textos com equals, nunca com =="). O Aluno não sobrescreveu, então duas fichas idênticas da Ana são "diferentes".
Sobrescrevendo o equals
Para a escola da Ana, dois alunos são o mesmo aluno quando têm a mesma matrícula. A forma padrão de escrever:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Aluno outro)) return false;
return matricula == outro.matricula;
}
- public boolean equals(Object o)
- A assinatura tem que ser exatamente esta: o parâmetro é
Object, nãoAluno. ComAluno, seria uma sobrecarga, não uma sobrescrita (e o@Overrideacusaria). - if (this == o) return true;
- Atalho: se é o mesmo objeto, é igual.
- if (!(o instanceof Aluno outro)) return false;
- Se
onão é umAluno(ou énull), não é igual. Se for, cria a variáveloutro, já convertida (oinstanceofcom padrão). - return matricula == outro.matricula;
- A regra de igualdade da classe. Para comparar textos aqui dentro, use
equals(ouObjects.equals(a, b), que aceitanull).
Com isso, a1.equals(a2) passa a dar true. Mas a história não acabou: falta o hashCode, e o próximo exemplo mostra por quê.
hashCode e o contrato com equals
O hashCode() devolve um int que "resume" o objeto. Coleções como HashSet e HashMap usam esse número para guardar os objetos em gavetas: em vez de comparar um objeto novo com todos os outros, elas calculam o hashCode, vão direto à gaveta daquele número e só ali usam o equals. É isso que as deixa rápidas.
Para funcionar, existe uma regra, chamada de contrato:
equals × hashCode
Se a.equals(b) é true, então a.hashCode() tem que ser igual a b.hashCode(). (O contrário não é exigido: objetos diferentes podem, por acaso, ter o mesmo hashCode.) Consequência prática: sobrescreveu equals, sobrescreva hashCode, usando os mesmos atributos.
O que acontece quando o contrato é quebrado
A Ana sobrescreveu o equals (por matrícula) e esqueceu o hashCode. Ela coloca a mesma aluna duas vezes num HashSet, um conjunto que não aceita repetidos:
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Aluno a1 = new Aluno("Ana", 123);
Aluno a2 = new Aluno("Ana", 123);
System.out.println("equals: " + a1.equals(a2));
Set<Aluno> turma = new HashSet<>();
turma.add(a1);
turma.add(a2);
System.out.println("Tamanho do conjunto: " + turma.size());
System.out.println("Contém a Ana? " + turma.contains(new Aluno("Ana", 123)));
}
}
class Aluno {
String nome;
int matricula;
Aluno(String nome, int matricula) {
this.nome = nome;
this.matricula = matricula;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Aluno outro)) return false;
return matricula == outro.matricula;
}
}
$ java Main.java equals: true « o equals diz que são a mesma aluna… Tamanho do conjunto: 2 « …mas o conjunto "sem repetidos" guardou as duas! Contém a Ana? false « e nem acha a Ana
Nenhum erro, nenhuma exceção: só respostas erradas. O que aconteceu: a1 e a2 usaram o hashCode de Object, que é diferente para cada objeto. O HashSet colocou cada uma numa gaveta diferente e nunca chegou a chamar o equals para compará-las. No contains, a terceira "Ana" foi procurada numa terceira gaveta, vazia.
O conserto é um hashCode baseado no mesmo atributo do equals:
@Override
public int hashCode() {
return Integer.hashCode(matricula);
}
$ java Main.java equals: true Tamanho do conjunto: 1 « agora sim Contém a Ana? true
Agora alunos com a mesma matrícula caem na mesma gaveta, e lá o equals decide. Quando a igualdade usa vários atributos, o jeito mais simples é java.util.Objects.hash, que combina todos:
@Override
public int hashCode() {
return Objects.hash(nome, matricula); // com import java.util.Objects;
}
Os records geram equals, hashCode e toString corretos, usando todos os componentes. E os editores (IntelliJ: Generate → equals() and hashCode(); VS Code: Source Action) escrevem os dois métodos juntos para uma classe comum. Gere os dois sempre juntos, com os mesmos atributos.
| Você sobrescreveu… | Resultado num HashSet/HashMap |
|---|---|
| Nenhum dos dois | Cada objeto é único (igualdade por ==). Consistente, mas "Anas" iguais contam como diferentes. |
Só equals | Errado: repetidos no conjunto, contains e get falham. (O caso da Ana.) |
Só hashCode | Inútil: mesma gaveta, mas o equals de Object ainda diz "diferentes". |
| Os dois, com os mesmos atributos | Correto |
getClass(): de que classe é este objeto?
O getClass() devolve um objeto do tipo Class que descreve a classe real do objeto, não o tipo da variável. Ele já apareceu no toString padrão e em Herança:
public class Main {
public static void main(String[] args) {
Animal a = new Cachorro();
System.out.println(a.getClass().getName());
System.out.println(a.getClass().getSimpleName());
System.out.println(a instanceof Animal);
System.out.println(a.getClass() == Animal.class);
System.out.println(a.getClass() == Cachorro.class);
System.out.println("texto".getClass().getName());
}
}
class Animal {
}
class Cachorro extends Animal {
}
$ java Main.java Cachorro « getName(): nome completo (aqui sem pacote) Cachorro « getSimpleName(): sem o pacote true « instanceof aceita subclasses false « getClass() é exato: Cachorro não é Animal.class true java.lang.String « com pacote, o nome completo aparece
- Cachorro.class
- Escrever
NomeDaClasse.classdá o objetoClassdaquela classe, para comparar. - getName() × getSimpleName()
- Com e sem o pacote:
java.lang.String×String.
A diferença em relação ao instanceof está nas duas linhas do meio: instanceof Animal responde "é um animal, de qualquer tipo?"; getClass() == Animal.class responde "é exatamente da classe Animal?". No dia a dia, prefira instanceof. O getClass() aparece em mensagens, em logs e é a porta de entrada da Reflexão.
Quando dá errado
NomeDaClasse@1a2b3c na tela
O que aconteceu: o objeto não tem toString() próprio. Acontece também com arrays: println(notas) mostra algo como [I@… (para arrays, use Arrays.toString(notas); veja A Classe Arrays).
Como resolver: sobrescreva public String toString().
equals dá false para objetos com os mesmos dados
O que aconteceu: a classe não sobrescreveu equals, e o de Object compara com ==. Outra causa: o método foi escrito como equals(Aluno o) (sobrecarga), e as coleções continuam chamando o equals(Object) herdado.
Como resolver: sobrescreva public boolean equals(Object o), com @Override para o compilador conferir a assinatura.
HashSet com repetidos, contains ou HashMap.get não acham o objeto
O que aconteceu: o contrato foi quebrado: equals sobrescrito sem hashCode (ou os dois usando atributos diferentes). Outra causa: um atributo usado no hashCode mudou depois de o objeto entrar no conjunto, e ele ficou "na gaveta errada".
Como resolver: sobrescreva hashCode com os mesmos atributos do equals (Objects.hash(…)); use atributos que não mudam (final), ou use um record.
Referência
| Método | Padrão de Object | Como sobrescrever |
|---|---|---|
toString() | Classe@hashEmHexadecimal | @Override public String toString() { return …; } |
equals(Object o) | Igual a == | this == o → instanceof → comparar os atributos |
hashCode() | Um número por objeto | Objects.hash(atributos do equals) |
getClass() | A classe real do objeto | Não pode (é final) |
a == b | O mesmo objeto? (não pode ser mudado) | |
Objects.equals(a, b) | Compara com equals, aceitando null dos dois lados | |
Exercícios
1. A assinatura certa
Complete a assinatura do equals que sobrescreve o de Object:
2. Hexadecimal
O toString padrão mostrou Produto@1f. Qual é o hashCode() desse objeto, em decimal?
3. O par
Sobrescreveu equals? Então qual outro método de Object precisa sobrescrever?
4. String a = new String("oi"); String b = new String("oi"); O que a == b e a.equals(b) valem?
5. Uma classe sobrescreveu só o equals. Dois objetos "iguais" são postos num HashSet. O que é mais provável?
6. Animal a = new Gato(); Qual expressão vale true?
Missão final: o Aluno tem equals por matrícula, mas o HashSet está guardando a Ana duas vezes. Rode e confira o tamanho 2. Depois sobrescreva public int hashCode() usando a matrícula (Integer.hashCode(matricula) ou Objects.hash(matricula), com o import). A saída deve ter Tamanho do conjunto: 1 e Contém a Ana? true.
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Aluno a1 = new Aluno("Ana", 123);
Aluno a2 = new Aluno("Ana", 123);
System.out.println("equals: " + a1.equals(a2));
Set<Aluno> turma = new HashSet<>();
turma.add(a1);
turma.add(a2);
System.out.println("Tamanho do conjunto: " + turma.size());
System.out.println("Contém a Ana? " + turma.contains(new Aluno("Ana", 123)));
}
}
class Aluno {
String nome;
int matricula;
Aluno(String nome, int matricula) {
this.nome = nome;
this.matricula = matricula;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Aluno outro)) return false;
return matricula == outro.matricula;
}
}
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Aluno a1 = new Aluno("Ana", 123);
Aluno a2 = new Aluno("Ana", 123);
System.out.println("equals: " + a1.equals(a2));
Set<Aluno> turma = new HashSet<>();
turma.add(a1);
turma.add(a2);
System.out.println("Tamanho do conjunto: " + turma.size());
System.out.println("Contém a Ana? " + turma.contains(new Aluno("Ana", 123)));
}
}
class Aluno {
String nome;
int matricula;
Aluno(String nome, int matricula) {
this.nome = nome;
this.matricula = matricula;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Aluno outro)) return false;
return matricula == outro.matricula;
}
@Override
public int hashCode() {
return Integer.hashCode(matricula);
}
}
Resumo
- Toda classe herda de
Objectos métodostoString,equals,hashCodeegetClass(entre outros). - O
toStringpadrão éClasse@hashCodeEmHexadecimal; sobrescreva para mostrar algo útil. ==pergunta "é o mesmo objeto?";equalspergunta "são iguais?". Oequalspadrão faz==; sobrescreva comequals(Object o).- Contrato: objetos
equalsprecisam ter o mesmohashCode. Sobrescreveu um, sobrescreva o outro, com os mesmos atributos. Senão,HashSeteHashMaperram em silêncio. getClass()dá a classe exata do objeto;instanceofaceita subclasses.- Records já trazem
equals,hashCodeetoStringprontos e corretos.