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.

Você vai aprender
  • Quais métodos todo objeto herda de Object.
  • O que significa o Aluno@5d20e46 do toString padrão.
  • A diferença entre == e equals, e como sobrescrever o equals.
  • O que é o hashCode, o contrato entre ele e o equals, 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étodoPara quêCostuma ser sobrescrito?
String toString()O objeto como textoSim, 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 HashMapSim, sempre junto com o equals
Class<?> getClass()A classe do objetoNão (é final)
clone()Copiar o objetoRaramente; prefira um construtor de cópia
wait(), notify(), notifyAll()Coordenação entre threadsNão (veja synchronized)
finalize()Antigo; obsoleto (deprecated) desde o Java 9Nã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). 5d20e46 em hexadecimal é 97652294 em 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 == b pergunta: "a e b são o mesmo objeto?" (apontam para o mesmo lugar da memória?)
  • a.equals(b) pergunta: "a e b sã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ão Aluno. Com Aluno, seria uma sobrecarga, não uma sobrescrita (e o @Override acusaria).
if (this == o) return true;
Atalho: se é o mesmo objeto, é igual.
if (!(o instanceof Aluno outro)) return false;
Se o não é um Aluno (ou é null), não é igual. Se for, cria a variável outro, já convertida (o instanceof com padrão).
return matricula == outro.matricula;
A regra de igualdade da classe. Para comparar textos aqui dentro, use equals (ou Objects.equals(a, b), que aceita null).

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:

O 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;
    }
Não quer escrever nada disso?

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 doisCada objeto é único (igualdade por ==). Consistente, mas "Anas" iguais contam como diferentes.
Só equalsErrado: repetidos no conjunto, contains e get falham. (O caso da Ana.)
Só hashCodeInútil: mesma gaveta, mas o equals de Object ainda diz "diferentes".
Os dois, com os mesmos atributosCorreto

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.class dá o objeto Class daquela 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

Aparece 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étodoPadrão de ObjectComo 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 objetoObjects.hash(atributos do equals)
getClass()A classe real do objetoNão pode (é final)
a == bO 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:

public boolean equals() { … }

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 Object os métodos toString, equals, hashCode e getClass (entre outros).
  • O toString padrão é Classe@hashCodeEmHexadecimal; sobrescreva para mostrar algo útil.
  • == pergunta "é o mesmo objeto?"; equals pergunta "são iguais?". O equals padrão faz ==; sobrescreva com equals(Object o).
  • Contrato: objetos equals precisam ter o mesmo hashCode. Sobrescreveu um, sobrescreva o outro, com os mesmos atributos. Senão, HashSet e HashMap erram em silêncio.
  • getClass() dá a classe exata do objeto; instanceof aceita subclasses.
  • Records já trazem equals, hashCode e toString prontos e corretos.