Java Sobrescrita (@Override)

Todo animal emite um som, mas cada um emite o seu. Se a classe Animal tem um método emitirSom(), o Cachorro herda esse método… e quer trocá-lo por um que late. Trocar a versão herdada de um método se chama sobrescrever (em inglês, override). Nesta aula você vai ver como fazer, quais são as regras e por que a anotação @Override deve estar em toda sobrescrita.

Você vai aprender
  • O que é sobrescrever um método e como o Java escolhe qual versão rodar.
  • O que a anotação @Override faz e o erro que ela pega: method does not override or implement a method from a supertype.
  • As regras: mesmo nome e parâmetros, visibilidade igual ou maior, tipo de retorno compatível.
  • A diferença entre sobrescrita e sobrecarga.
  • Como sobrescrever o toString() para seus objetos aparecerem bonitos no println.

Você vai precisar de Herança (extends) e, para chamar a versão da superclasse, de super.


A ideia: a receita da família, com o seu toque

A avó da Ana tem uma receita de bolo que passou para toda a família. A mãe usa a receita igualzinha. A Ana usa a mesma receita, mas troca a cobertura por chocolate. O nome continua "bolo da família", e quem pede o bolo à Ana recebe a versão dela.

Na programação é igual. A superclasse define um método; cada subclasse pode usá-lo como está (herdar) ou escrever a sua versão, com o mesmo nome e os mesmos parâmetros (sobrescrever). Quando o método é chamado num objeto, roda a versão da classe daquele objeto.


Sobrescrevendo emitirSom()

public class Main {
    public static void main(String[] args) {
        Animal bicho = new Animal();
        Cachorro rex = new Cachorro();
        Gato mimi = new Gato();
        bicho.emitirSom();
        rex.emitirSom();
        mimi.emitirSom();
    }
}

class Animal {
    void emitirSom() {
        System.out.println("(algum som de animal)");
    }
}

class Cachorro extends Animal {
    @Override
    void emitirSom() {
        System.out.println("Au au!");
    }
}

class Gato extends Animal {
    @Override
    void emitirSom() {
        System.out.println("Miau!");
    }
}
void emitirSom()
No Animal: a versão "padrão", que toda subclasse herda.
@Override
"Sobrescreve". Uma anotação (as palavras com @ são anotações, avisos para o compilador; veja Anotações). Ela diz: "este método substitui um herdado; confira".
void emitirSom()
No Cachorro: exatamente a mesma assinatura (nome + parâmetros), com outro corpo.
System.out.println("Au au!");
O comportamento novo.
$ java Main.java
(algum som de animal)       « o Animal usa a própria versão
Au au!                      « o Cachorro usa a dele
Miau!                       « o Gato usa a dele

A mesma chamada, emitirSom(), teve três resultados, dependendo do objeto. A versão do Animal continua existindo: ela só não é usada pelos objetos que têm a própria. E, se quiser, a subclasse ainda pode chamá-la com super.emitirSom().

Isso fica ainda mais interessante quando uma variável do tipo Animal guarda um Cachorro: qual versão roda? Esse é o assunto da próxima aula, Polimorfismo, e é a razão de a sobrescrita ser tão importante.

Missão: crie a classe Vaca, que herda de Animal e sobrescreve emitirSom() para mostrar Muu!. Use @Override. No main, crie uma vaca e chame o método.

public class Main {
    public static void main(String[] args) {
        Animal bicho = new Animal();
        bicho.emitirSom();
        // crie uma Vaca e chame emitirSom()
    }
}

class Animal {
    void emitirSom() {
        System.out.println("(algum som de animal)");
    }
}

// crie aqui a classe Vaca
public class Main {
    public static void main(String[] args) {
        Animal bicho = new Animal();
        bicho.emitirSom();
        Vaca mimosa = new Vaca();
        mimosa.emitirSom();
    }
}

class Animal {
    void emitirSom() {
        System.out.println("(algum som de animal)");
    }
}

class Vaca extends Animal {
    @Override
    void emitirSom() {
        System.out.println("Muu!");
    }
}

@Override: o seguro contra erros de digitação

O @Override é opcional: sem ele, a sobrescrita funciona do mesmo jeito. Então por que usar? Porque ele transforma um erro silencioso num erro de compilação. Veja o que acontece quando a Ana digita emitirSon, com n, no Gato, sem @Override:

class Gato extends Animal {
    void emitirSon() {
        System.out.println("Miau!");
    }
}
$ java Main.java
(algum som de animal)
Au au!
(algum som de animal)       « o gato não miou!

Nenhum erro. O Java entendeu que emitirSon() é um método novo do Gato, que ninguém chama. O mimi.emitirSom() do main usou a versão herdada do Animal. Num programa grande, um bug assim pode levar horas para ser achado. Agora, a mesma digitação errada com @Override:

Este código tem um erro de propósito
class Gato extends Animal {
    @Override
    void emitirSon() {
        System.out.println("Miau!");
    }
}
$ javac Main.java
Main.java:26: error: method does not override or implement a method from a supertype
    @Override
    ^
1 error

(O número da linha, 26, é o do arquivo completo do começo da aula, com o Gato trocado.) Method does not override or implement a method from a supertype = "o método não sobrescreve nem implementa um método de um supertipo". Traduzindo para a situação: "você disse que isto é uma sobrescrita, mas não existe emitirSon() em nenhuma classe acima". O erro aparece na hora, apontando o lugar exato. A regra prática: toda sobrescrita leva @Override. Os editores (IntelliJ, VS Code) já escrevem a anotação quando você pede para sobrescrever um método.


As regras da sobrescrita

Para um método da subclasse sobrescrever um da superclasse, ele precisa ser "compatível". O compilador confere quatro coisas:

RegraPodeNão pode
Nome e parâmetros (a assinatura)Exatamente iguaisParâmetros diferentes: aí é outro método (sobrecarga)
VisibilidadeIgual ou maior (de pacote → public)Menor (public → de pacote ou private)
Tipo de retornoO mesmo, ou um subtipo deleUm tipo incompatível
O método da superclasseComum (de instância)final, private ou static

Visibilidade: não pode diminuir

Se o Animal promete que emitirSom() é public (qualquer um pode chamar), o Cachorro não pode esconder o método. Quem recebe "um animal" confia na promessa da superclasse:

Este código tem um erro de propósito
class Animal {
    public void emitirSom() {
        System.out.println("(algum som)");
    }
}

class Cachorro extends Animal {
    @Override
    void emitirSom() {
        System.out.println("Au au!");
    }
}
$ javac Main.java
Main.java:14: error: emitirSom() in Cachorro cannot override emitirSom() in Animal
    void emitirSom() {
         ^
  attempting to assign weaker access privileges; was public
1 error

"emitirSom() em Cachorro não pode sobrescrever emitirSom() em Animal: tentativa de atribuir privilégios de acesso mais fracos; era public." O conserto é escrever public void emitirSom() também no Cachorro. Aumentar é permitido: um método sem modificador na superclasse pode virar public na subclasse.

Tipo de retorno: o mesmo (ou mais específico)

Este código tem um erro de propósito
class Animal {
    String nome() {
        return "animal";
    }
}

class Cachorro extends Animal {
    @Override
    int nome() {
        return 42;
    }
}
$ javac Main.java
Main.java:14: error: nome() in Cachorro cannot override nome() in Animal
    int nome() {
        ^
  return type int is not compatible with String
Main.java:13: error: method does not override or implement a method from a supertype
    @Override
    ^
2 errors

"O tipo de retorno int não é compatível com String." Quem chama nome() num animal espera um texto. O segundo erro é consequência do primeiro: como a sobrescrita falhou, o @Override também reclama.

Mas há uma liberdade: o retorno pode ser mais específico. Se o Animal tem Animal criarFilhote(), o Cachorro pode declarar Cachorro criarFilhote(), porque todo Cachorro é um Animal. Isso se chama retorno covariante:

public class Main {
    public static void main(String[] args) {
        Cachorro rex = new Cachorro();
        Cachorro filhote = rex.criarFilhote();
        filhote.emitirSom();
    }
}

class Animal {
    Animal criarFilhote() {
        return new Animal();
    }

    void emitirSom() {
        System.out.println("(algum som)");
    }
}

class Cachorro extends Animal {
    @Override
    Cachorro criarFilhote() {
        return new Cachorro();
    }

    @Override
    void emitirSom() {
        System.out.println("Au!");
    }
}
$ java Main.java
Au!

Graças ao retorno Cachorro, o main guarda o filhote numa variável Cachorro, sem conversão nenhuma.

O que não pode ser sobrescrito

  • Métodos final: travados de propósito (erro overridden method is final; veja final).
  • Métodos private: a subclasse nem os enxerga. Um método com o mesmo nome na subclasse é outro método, sem relação; com @Override, dá o erro method does not override….
  • Métodos static: são da classe, não do objeto (veja static). Um static de mesmo nome na subclasse esconde o da superclasse, mas não há escolha pelo objeto, e o @Override recusa.
  • Construtores: não são herdados, então não há o que sobrescrever.

Sobrescrita × sobrecarga

Os nomes parecem, e muita gente confunde. A sobrecarga (overloading) é ter vários métodos com o mesmo nome e parâmetros diferentes, como Math.max(int, int) e Math.max(double, double). A sobrescrita (overriding) é a subclasse substituir um método com os mesmos parâmetros.

Sobrecarga (overload)Sobrescrita (override)
OndeNa mesma classe (ou herdada)Numa subclasse
ParâmetrosDiferentesIguais
ResultadoMétodos novos, que convivemA versão nova substitui a herdada
Quem escolheO compilador, pelos argumentosA JVM, pelo objeto (ao rodar)
@OverrideNão se aplica (dá erro)Sempre use

Um engano comum: querer sobrescrever, mas mudar os parâmetros. O @Override pega:

$ javac Main.java
Main.java:13: error: method does not override or implement a method from a supertype
    @Override
    ^
1 error

Aqui, o Cachorro escreveu emitirSom(int vezes), mas o Animal só tem emitirSom(), sem parâmetros. Sem o @Override, o código compilaria, e o Cachorro teria dois métodos: o herdado e o novo, sobrecarregado.


Sobrescrevendo toString()

Este é, de longe, o método mais sobrescrito do Java. Lembra que toda classe herda de Object (veja Herança)? Um dos métodos que vêm de lá é o toString(), "para texto". O println chama esse método sempre que recebe um objeto, e a concatenação com + também. A versão de Object não é nada amigável:

$ java Main.java
Aluno@365c30cc
Destaque: Aluno@365c30cc

(O que é esse @365c30cc é explicado em A Classe Object.) Sobrescrevendo, a Ana decide como um Aluno vira texto:

public class Main {
    public static void main(String[] args) {
        Aluno ana = new Aluno("Ana", 9.5);
        System.out.println(ana);
        System.out.println("Destaque: " + ana);
    }
}

class Aluno {
    String nome;
    double nota;

    Aluno(String nome, double nota) {
        this.nome = nome;
        this.nota = nota;
    }

    @Override
    public String toString() {
        return nome + " (nota " + nota + ")";
    }
}
@Override
Confere que toString() existe acima (existe, em Object).
public
Obrigatório: em Object, toString() é public, e a visibilidade não pode diminuir.
String toString()
Devolve um texto; não recebe nada.
return nome + " (nota " + nota + ")";
O texto que representa o objeto. O método devolve o texto, não imprime.
$ java Main.java
Ana (nota 9.5)
Destaque: Ana (nota 9.5)        « o + também usa o toString()

O esquecimento mais comum é o public. Sem ele, o erro é o da regra de visibilidade, agora com Object:

$ javac Main.java
Main.java:19: error: toString() in Aluno cannot override toString() in Object
    String toString() {
           ^
  attempting to assign weaker access privileges; was public
1 error
Toda classe que você cria merece um toString()

Ele não serve só para mostrar na tela: quando você imprime uma lista de alunos, ou quando um erro mostra um objeto, é o toString() que aparece. Os records já vêm com um pronto. Os outros dois métodos de Object que costumam ser sobrescritos, equals e hashCode, ficam para A Classe Object.


Quando dá errado

"method does not override or implement a method from a supertype"
$ javac Main.java
Main.java:26: error: method does not override or implement a method from a supertype
    @Override
    ^
1 error

O que aconteceu: o método marcado com @Override não corresponde a nenhum método herdado. Causas: erro de digitação no nome (emitirSon, tostring, ToString), parâmetros diferentes, a classe esqueceu o extends, ou o método da superclasse é private ou static.

Como resolver: compare, letra por letra, o nome e os parâmetros com os do método da superclasse. Não apague o @Override para o erro sumir: ele está avisando de um bug real.

"attempting to assign weaker access privileges; was public"

O que aconteceu: a versão da subclasse é menos visível que a da superclasse. É quase sempre o public esquecido em toString(), equals() ou num método de interface.

Como resolver: use a mesma visibilidade (ou maior) que a superclasse: public String toString().

"return type … is not compatible with …"

O que aconteceu: o tipo de retorno da sobrescrita não é o mesmo nem um subtipo do original.

Como resolver: devolva o mesmo tipo. Se precisa devolver outra coisa, o método é outro: dê outro nome.

Compilou, mas a versão da subclasse não roda

O que aconteceu: sem @Override, um nome digitado errado ou parâmetros diferentes criaram um método novo, e a versão herdada continua sendo chamada.

Como resolver: coloque @Override; o compilador vai apontar o problema.


Referência

Escrita / termoSignificado
Sobrescrever (override)A subclasse escreve a sua versão de um método herdado, com a mesma assinatura
@OverridePede ao compilador para conferir que é mesmo uma sobrescrita
super.metodo()Chama a versão da superclasse, de dentro da subclasse
VisibilidadeIgual ou maior que a original
RetornoO mesmo tipo ou um subtipo (covariante)
Não sobrescrevíveisMétodos final, private, static e construtores
public String toString()O texto que representa o objeto (usado por println e +)

Exercícios

1. A anotação

Complete a linha que vem antes de um método que sobrescreve outro:

2. O toString completo

Complete a primeira linha do toString de uma classe:

return nome; }

3. Aumentar a visibilidade

Na superclasse, o método é void abrir() (de pacote). Qual modificador a subclasse pode usar para deixá-lo visível a todos?

void abrir() { … }

4. Animal tem void andar(). Pato (que estende Animal) escreve void andar(int passos). Isso é…

5. Para que serve o @Override?

6. Por que String toString() { … } (sem public) não compila?

Missão final: a Ana escreveu um toString() para Produto, mas o programa mostra Produto@…. Rode para ver. Coloque @Override em cima do método e rode de novo: o compilador vai apontar o problema. Conserte o nome do método. A saída deve ser Café - R$ 18.9.

public class Main {
    public static void main(String[] args) {
        Produto cafe = new Produto("Café", 18.90);
        System.out.println(cafe);
    }
}

class Produto {
    String nome;
    double preco;

    Produto(String nome, double preco) {
        this.nome = nome;
        this.preco = preco;
    }

    public String tostring() {
        return nome + " - R$ " + preco;
    }
}
public class Main {
    public static void main(String[] args) {
        Produto cafe = new Produto("Café", 18.90);
        System.out.println(cafe);
    }
}

class Produto {
    String nome;
    double preco;

    Produto(String nome, double preco) {
        this.nome = nome;
        this.preco = preco;
    }

    @Override
    public String toString() {
        return nome + " - R$ " + preco;
    }
}

Resumo

  • Sobrescrever é a subclasse escrever a sua versão de um método herdado, com o mesmo nome e os mesmos parâmetros. Roda a versão da classe do objeto.
  • Use sempre @Override: sem ele, um nome errado vira um método novo, em silêncio; com ele, vira o erro method does not override or implement a method from a supertype.
  • Regras: visibilidade igual ou maior (weaker access privileges), retorno igual ou mais específico, e nada de sobrescrever métodos final, private ou static.
  • Sobrecarga = parâmetros diferentes, métodos que convivem. Sobrescrita = mesmos parâmetros, versão que substitui.
  • Sobrescreva public String toString() para seus objetos aparecerem como texto legível.