Java Classes Abstratas
Na aula de Polimorfismo, a classe Forma tinha um método area() que devolvia 0. Era uma gambiarra: "forma" é uma ideia geral, sem área definida. Só um retângulo ou um círculo de verdade têm área. As classes abstratas são o jeito certo de escrever essas ideias gerais: uma classe que serve de modelo para as subclasses, que não pode virar objeto sozinha e que pode obrigar as filhas a escrever certos métodos.
- O que é uma classe abstrata (
abstract class) e um método abstrato. - Por que não dá para criar um objeto de uma classe abstrata (o erro real).
- Como o compilador obriga as subclasses a implementar os métodos abstratos.
- Como misturar métodos abstratos e métodos prontos na mesma classe.
- Quando usar uma classe abstrata e quando usar uma interface.
A ideia: "instrumento musical"
Numa loja de música, você pode comprar um violão, um piano, uma flauta. Mas não pode comprar "um instrumento musical", genérico. "Instrumento" é uma categoria: diz o que todos têm em comum (todo instrumento toca, tem um preço, tem uma marca), mas não existe sozinho. E, embora todo instrumento toque, cada um toca de um jeito: não dá para escrever o "tocar" de um instrumento genérico.
Em Java:
- A categoria é uma classe abstrata: não se cria objeto dela (
new Instrumento()é proibido). - O "todo instrumento toca, mas cada um do seu jeito" é um método abstrato: declarado na classe abstrata, sem corpo, e escrito por cada subclasse.
A palavra abstract vem do latim abstractus, "separado, retirado": algo pensado em geral, separado dos casos concretos. O oposto, nas aulas de Java, é concreto: uma classe comum, de que se pode criar objetos.
Uma Forma abstrata
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Forma> formas = List.of(new Retangulo("Tapete", 2, 3), new Circulo("Mesa", 1));
for (Forma f : formas) {
System.out.println(f.descrever());
}
}
}
abstract class Forma {
String nome;
Forma(String nome) {
this.nome = nome;
}
abstract double area();
String descrever() {
return nome + ": área " + area();
}
}
class Retangulo extends Forma {
double largura, altura;
Retangulo(String nome, double largura, double altura) {
super(nome);
this.largura = largura;
this.altura = altura;
}
@Override
double area() {
return largura * altura;
}
}
class Circulo extends Forma {
double raio;
Circulo(String nome, double raio) {
super(nome);
this.raio = raio;
}
@Override
double area() {
return Math.PI * raio * raio;
}
}
- abstract class Forma
- Uma classe abstrata. Pode ter atributos, construtores e métodos como qualquer classe, mas não pode ser instanciada com
new. - Forma(String nome)
- Sim, classe abstrata tem construtor! Ele é chamado pelas subclasses com
super(nome)(veja super), para montar a "camada"Formade cada objeto. - abstract double area();
- Um método abstrato: só a assinatura, terminada em
;, sem chaves e sem corpo. Ele diz "toda forma tem área", sem dizer como calcular. - String descrever()
- Um método comum (concreto), pronto, que todas as subclasses herdam. Repare que ele usa
area(), mesmo sem saber como ela é calculada. - @Override double area()
- Em cada subclasse, a versão concreta. Escrever um método abstrato herdado também se chama implementar o método.
$ java Main.java Tapete: área 6.0 Mesa: área 3.141592653589793 « π × 1 × 1
Veja como as peças se encaixam: o descrever() foi escrito uma vez, em Forma. Ao rodar, ele chama area(), e o despacho dinâmico escolhe a versão do objeto: a do retângulo para o tapete, a do círculo para a mesa. A classe abstrata define o "roteiro"; as subclasses preenchem a parte que muda.
Missão: crie a classe Quadrado, que herda de Forma, recebe nome e lado no construtor (chamando super(nome)) e implementa area() como lado * lado. Acrescente new Quadrado("Azulejo", 2) à lista. A saída deve ter Azulejo: área 4.0. Para ver a obrigação em ação, rode também uma vez com a classe Quadrado sem o método area().
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Forma> formas = List.of(new Retangulo("Tapete", 2, 3));
for (Forma f : formas) {
System.out.println(f.descrever());
}
}
}
abstract class Forma {
String nome;
Forma(String nome) {
this.nome = nome;
}
abstract double area();
String descrever() {
return nome + ": área " + area();
}
}
class Retangulo extends Forma {
double largura, altura;
Retangulo(String nome, double largura, double altura) {
super(nome);
this.largura = largura;
this.altura = altura;
}
@Override
double area() {
return largura * altura;
}
}
// crie aqui a classe Quadrado
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Forma> formas = List.of(new Retangulo("Tapete", 2, 3), new Quadrado("Azulejo", 2));
for (Forma f : formas) {
System.out.println(f.descrever());
}
}
}
abstract class Forma {
String nome;
Forma(String nome) {
this.nome = nome;
}
abstract double area();
String descrever() {
return nome + ": área " + area();
}
}
class Retangulo extends Forma {
double largura, altura;
Retangulo(String nome, double largura, double altura) {
super(nome);
this.largura = largura;
this.altura = altura;
}
@Override
double area() {
return largura * altura;
}
}
class Quadrado extends Forma {
double lado;
Quadrado(String nome, double lado) {
super(nome);
this.lado = lado;
}
@Override
double area() {
return lado * lado;
}
}
Não dá para instanciar
Uma forma genérica não tem área. Se fosse possível criar uma, o que descrever() mostraria? O compilador corta o problema pela raiz:
import java.util.List;
public class Main {
public static void main(String[] args) {
Forma f = new Forma("Coisa");
}
}
// … as classes Forma, Retangulo e Circulo de antes
$ javac Main.java
Main.java:5: error: Forma is abstract; cannot be instantiated
Forma f = new Forma("Coisa");
^
1 error
Forma is abstract; cannot be instantiated = "Forma é abstrata; não pode ser instanciada" (instanciar = criar uma instância, um objeto). Repare onde está a seta: no new Forma. A variável do tipo Forma é permitida, e é muito útil: Forma f = new Circulo("Mesa", 1); funciona, e é assim que o polimorfismo usa as classes abstratas.
Com abstract class Forma | Pode? |
|---|---|
new Forma("x") | Não |
Forma f = new Circulo("Mesa", 1); | Sim |
List<Forma>, void desenhar(Forma f) | Sim |
class Circulo extends Forma | Sim (é para isso que ela existe) |
A obrigação das subclasses
O método abstrato é uma promessa: "toda forma concreta terá area()". Por isso, uma subclasse concreta que esquece de implementá-lo não compila. A Ana escreveu o Circulo sem o area():
$ javac Main.java
Main.java:41: error: Circulo is not abstract and does not override abstract method area() in Forma
class Circulo extends Forma {
^
1 error
"Circulo não é abstrata e não sobrescreve o método abstrato area() de Forma." A mensagem já sugere as duas saídas:
- Implementar o método (o caso normal): escrever
double area()noCirculo. - Declarar a subclasse também abstrata:
abstract class Poligono extends Formapode deixararea()para as filhas dela (Triangulo,Hexagono…). A obrigação só é cobrada da primeira classe concreta da descendência.
Essa é a grande vantagem sobre a "gambiarra" do return 0 da aula anterior: lá, quem esquecesse de sobrescrever teria uma área zero, em silêncio. Aqui, o programa nem compila.
As regras dos métodos abstratos
Método abstrato não tem corpo
Nem chaves vazias. Um abstract com corpo é uma contradição:
$ javac Main.java
Main.java:19: error: abstract methods cannot have a body
abstract double area() {
^
1 error
"Métodos abstratos não podem ter corpo." Troque { … } por ;.
Método abstrato só em classe abstrata
Se a classe tem um método abstrato, ela precisa ser abstrata (senão daria para criar um objeto com um método "em branco"). Tirando o abstract da classe Forma:
$ javac Main.java
Main.java:12: error: Forma is not abstract and does not override abstract method area() in Forma
class Forma {
^
1 error
A mensagem é a mesma da subclasse esquecida, agora apontando a própria Forma. O contrário é permitido: uma classe abstrata sem nenhum método abstrato. Isso serve para impedir que alguém crie objetos de uma classe que só existe para ser herdada.
abstract e final não combinam
abstract quer dizer "precisa ser herdada"; final quer dizer "não pode ser herdada" (veja final). Juntos:
$ javac Main.java
Main.java:12: error: illegal combination of modifiers: abstract and final
abstract final class Forma {
^
Main.java:26: error: cannot inherit from final Forma
class Retangulo extends Forma {
^
Main.java:41: error: cannot inherit from final Forma
class Circulo extends Forma {
^
3 errors
"Combinação ilegal de modificadores." O mesmo vale para métodos: um método abstrato não pode ser final, private (a subclasse não o enxergaria para implementá-lo) nem static (não haveria objeto para escolher a versão).
Classe abstrata × interface
A próxima aula apresenta as interfaces, que também declaram métodos sem corpo e também não podem ser instanciadas. Então, quando usar cada uma? A diferença principal:
- Uma classe abstrata é uma "mãe incompleta": tem estado (atributos como
nome), construtores e código pronto para compartilhar. Uma classe só pode herdar de uma. - Uma interface é um "contrato de habilidade": diz o que uma classe sabe fazer (
Comparable: "sei me comparar"). Não tem atributos de objeto, e uma classe pode implementar várias.
| Situação | Use |
|---|---|
| As classes são da mesma família ("é um") e compartilham atributos e código | Classe abstrata (Forma, Funcionario, Conta) |
| Classes sem parentesco precisam de uma mesma habilidade | Interface (Comparable, Imprimivel, Pagavel) |
| A classe precisa de mais de um "tipo pai" | Interfaces (só há uma superclasse) |
| Há um construtor ou atributos que todas as filhas precisam | Classe abstrata |
Muitas vezes as duas aparecem juntas: uma interface diz o que fazer, e uma classe abstrata implementa a parte comum para facilitar a vida das subclasses. A comparação completa, em tabela, está em Interfaces.
Quando dá errado
$ javac Main.java
Main.java:5: error: Forma is abstract; cannot be instantiated
Forma f = new Forma("Coisa");
^
1 error
O que aconteceu: um new numa classe abstrata.
Como resolver: crie um objeto de uma subclasse concreta (new Circulo(…)). A variável pode continuar do tipo Forma. (Também é possível criar uma subclasse "na hora", sem nome, com uma classe anônima.)
$ javac Main.java
Main.java:41: error: Circulo is not abstract and does not override abstract method area() in Forma
class Circulo extends Forma {
^
1 error
O que aconteceu: uma classe concreta herdou um método abstrato e não o escreveu. Se o nome do método aparece com algo estranho, confira se a assinatura é idêntica (um area(int x) não conta).
Como resolver: implemente o método com @Override, ou declare a classe como abstract se ela também for uma ideia geral.
O que aconteceu: o método abstrato ganhou chaves.
Como resolver: se ele é abstrato, termine com ;. Se você quer um comportamento padrão que as filhas podem trocar, tire o abstract e deixe o corpo: aí é um método comum, que pode ser sobrescrito.
Referência
| Escrita | Significado |
|---|---|
abstract class Forma { … } | Classe que não pode ser instanciada; serve de modelo |
abstract double area(); | Método sem corpo; as subclasses concretas são obrigadas a implementar |
| Método concreto em classe abstrata | Código pronto, herdado por todas as subclasses (pode chamar os abstratos) |
| Construtor em classe abstrata | Permitido; chamado pelas subclasses com super(…) |
abstract class B extends A | Subclasse abstrata: pode deixar os métodos abstratos para as filhas |
abstract + final/private/static | Combinações proibidas |
Exercícios
1. A classe-modelo
Complete para Instrumento não poder ser instanciado:
2. Método sem corpo
Complete a declaração do método abstrato tocar, que não recebe nada e não devolve nada:
3. Variável de tipo abstrato
Instrumento é abstrata e Violao estende Instrumento. Complete com o objeto que pode ser criado:
4. O que uma classe abstrata não pode ter?
5. Circulo extends Forma, Forma tem abstract double area(); e o Circulo não escreve area(). O que acontece?
6. Cachorro, Gato e Vaca compartilham os atributos nome e idade e o método comer(). O que é mais adequado para Animal?
Missão final: a loja tem funcionários mensalistas (salário fixo) e horistas (horas × valor da hora). Hoje, Funcionario é uma classe comum com salario() devolvendo 0, e o main cria um "funcionário genérico" sem sentido. Transforme: (1) Funcionario em classe abstrata; (2) salario() em método abstrato; (3) apague a linha do generico; (4) implemente salario() em Horista (horas * valorHora). A saída deve ter Caio: R$ 1600.0 e Bia: R$ 5000.0.
import java.util.List;
public class Main {
public static void main(String[] args) {
Funcionario generico = new Funcionario("Ninguém");
List<Funcionario> equipe = List.of(new Horista("Caio", 80, 20), new Mensalista("Bia", 5000));
for (Funcionario f : equipe) {
System.out.println(f.nome + ": R$ " + f.salario());
}
}
}
class Funcionario {
String nome;
Funcionario(String nome) {
this.nome = nome;
}
double salario() {
return 0;
}
}
class Mensalista extends Funcionario {
double fixo;
Mensalista(String nome, double fixo) {
super(nome);
this.fixo = fixo;
}
@Override
double salario() {
return fixo;
}
}
class Horista extends Funcionario {
double horas, valorHora;
Horista(String nome, double horas, double valorHora) {
super(nome);
this.horas = horas;
this.valorHora = valorHora;
}
}
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Funcionario> equipe = List.of(new Horista("Caio", 80, 20), new Mensalista("Bia", 5000));
for (Funcionario f : equipe) {
System.out.println(f.nome + ": R$ " + f.salario());
}
}
}
abstract class Funcionario {
String nome;
Funcionario(String nome) {
this.nome = nome;
}
abstract double salario();
}
class Mensalista extends Funcionario {
double fixo;
Mensalista(String nome, double fixo) {
super(nome);
this.fixo = fixo;
}
@Override
double salario() {
return fixo;
}
}
class Horista extends Funcionario {
double horas, valorHora;
Horista(String nome, double horas, double valorHora) {
super(nome);
this.horas = horas;
this.valorHora = valorHora;
}
@Override
double salario() {
return horas * valorHora;
}
}
Resumo
abstract class= uma ideia geral, que serve de modelo: tem atributos, construtores e métodos, mas não pode ser instanciada (… is abstract; cannot be instantiated).abstractnum método = só a assinatura, terminada em;. Toda subclasse concreta é obrigada a implementá-lo (… does not override abstract method …).- Métodos concretos da classe abstrata podem chamar os abstratos: a mãe escreve o roteiro, as filhas preenchem as partes que mudam.
- Variáveis, listas e parâmetros do tipo abstrato são o normal:
Forma f = new Circulo(…). - Classe abstrata: família com estado e código em comum (só uma superclasse). Interface: habilidade que várias classes, mesmo sem parentesco, podem ter. Próxima aula: Interfaces.