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.
- O que é sobrescrever um método e como o Java escolhe qual versão rodar.
- O que a anotação
@Overridefaz 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 noprintln.
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:
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:
| Regra | Pode | Não pode |
|---|---|---|
| Nome e parâmetros (a assinatura) | Exatamente iguais | Parâmetros diferentes: aí é outro método (sobrecarga) |
| Visibilidade | Igual ou maior (de pacote → public) | Menor (public → de pacote ou private) |
| Tipo de retorno | O mesmo, ou um subtipo dele | Um tipo incompatível |
| O método da superclasse | Comum (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:
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)
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 (errooverridden 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 erromethod does not override…. - Métodos
static: são da classe, não do objeto (veja static). Umstaticde mesmo nome na subclasse esconde o da superclasse, mas não há escolha pelo objeto, e o@Overriderecusa. - 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) | |
|---|---|---|
| Onde | Na mesma classe (ou herdada) | Numa subclasse |
| Parâmetros | Diferentes | Iguais |
| Resultado | Métodos novos, que convivem | A versão nova substitui a herdada |
| Quem escolhe | O compilador, pelos argumentos | A JVM, pelo objeto (ao rodar) |
@Override | Nã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, emObject). - 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
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
$ 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.
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().
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.
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 / termo | Significado |
|---|---|
| Sobrescrever (override) | A subclasse escreve a sua versão de um método herdado, com a mesma assinatura |
@Override | Pede ao compilador para conferir que é mesmo uma sobrescrita |
super.metodo() | Chama a versão da superclasse, de dentro da subclasse |
| Visibilidade | Igual ou maior que a original |
| Retorno | O mesmo tipo ou um subtipo (covariante) |
| Não sobrescrevíveis | Mé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:
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?
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 erromethod 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étodosfinal,privateoustatic. - 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.