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.

Você vai aprender
  • 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" Forma de 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:

Este código tem um erro de propósito
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 FormaPode?
new Forma("x")Não
Forma f = new Circulo("Mesa", 1);Sim
List<Forma>, void desenhar(Forma f)Sim
class Circulo extends FormaSim (é 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:

  1. Implementar o método (o caso normal): escrever double area() no Circulo.
  2. Declarar a subclasse também abstrata: abstract class Poligono extends Forma pode deixar area() 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çãoUse
As classes são da mesma família ("é um") e compartilham atributos e códigoClasse abstrata (Forma, Funcionario, Conta)
Classes sem parentesco precisam de uma mesma habilidadeInterface (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 precisamClasse 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

"… is abstract; cannot be instantiated"
$ 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.)

"… is not abstract and does not override abstract method … in …"
$ 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.

"abstract methods cannot have a body"

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

EscritaSignificado
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 abstrataCódigo pronto, herdado por todas as subclasses (pode chamar os abstratos)
Construtor em classe abstrataPermitido; chamado pelas subclasses com super(…)
abstract class B extends ASubclasse abstrata: pode deixar os métodos abstratos para as filhas
abstract + final/private/staticCombinações proibidas

Exercícios

1. A classe-modelo

Complete para Instrumento não poder ser instanciado:

Instrumento { }

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:

Instrumento i = ;

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).
  • abstract num 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.