Abstração em Java
Oculte detalhes de implementação com tipos abstratos em Java usando classes abstratas e interfaces.
Abstração é o quarto pilar da POO: descrever o que algo faz sem se comprometer com como. Você declara as operações que um tipo suporta, deixa a implementação para as classes concretas e escreve o restante do seu programa contra o tipo abstrato. Este capítulo é a visão conceitual — os dois mecanismos do Java para isso, abstract class e interface, têm cada um seu próprio capítulo dedicado.
As duas perguntas
Toda declaração de tipo em Java responde duas perguntas:
- O que os chamadores podem fazer com valores desse tipo? (sua API)
- Como cada uma dessas operações é implementada? (seu corpo)
Uma classe concreta responde as duas. Um tipo abstrato responde apenas a primeira e deixa a segunda para os subtipos:
public interface Shape {
double area(); // what — every Shape has an area
}
public class Circle implements Shape {
double r;
public Circle(double r) { this.r = r; }
public double area() { return Math.PI * r * r; } // how
}
public class Square implements Shape {
double side;
public Square(double side) { this.side = side; }
public double area() { return side * side; }
}Shape diz "toda forma tem uma área." Circle e Square dizem como calculá-la. O código que recebe um Shape não se importa com qual:
double sumAreas(List<Shape> shapes) {
double sum = 0;
for (Shape s : shapes) sum += s.area();
return sum;
}Esta função é fechada sobre a abstração. Funciona para Circle e Square hoje; para Triangle amanhã; para Polygon daqui a seis meses. Nenhum dos novos tipos requer qualquer alteração em sumAreas.
Os dois mecanismos do Java
| Mecanismo | O que fornece | Quando usar |
|---|---|---|
| abstract class | Uma classe parcial — alguns métodos abstratos, outros com corpos, além de campos e construtores | Quando os subtipos compartilharão estado e código de infraestrutura |
| interface | Um contrato puro (ou quase puro) — métodos que as classes implementadoras devem fornecer; sem estado de instância | Quando os subtipos só precisam concordar com um conjunto de operações e podem não ter mais nada em comum |
Uma classe estende uma única classe abstrata. Uma classe pode implementar muitas interfaces. Essa assimetria orienta muitos projetos: se você quiser "herança múltipla," as interfaces geralmente são a resposta.
Classes abstratas — implementação parcial
abstract em uma classe significa "você não pode instanciar isso diretamente — apenas subclasses." abstract em um método significa "sem corpo aqui; toda subclasse concreta deve fornecer um":
public abstract class Shape {
public abstract double area(); // every Shape must define this
// a concrete method, shared across all shapes
public final String describe() {
return getClass().getSimpleName() + " area=" + area();
}
}new Shape() é um erro de compilação. new Circle() funciona. Dentro de describe, a chamada area() despacha para a implementação da subclasse real — o mesmo mecanismo de polimorfismo que qualquer método substituído.
Use uma classe abstrata quando os subtipos realmente compartilham código. Se você se pegar escrevendo o mesmo auxiliar em três subclasses, é um sinal para elevá-lo ao pai.
Interfaces — o contrato
Uma interface declara operações e deixa a implementação inteiramente para quem a implementa:
public interface Comparable<T> {
int compareTo(T other);
}
public class Money implements Comparable<Money> {
private final long cents;
public int compareTo(Money other) {
return Long.compare(this.cents, other.cents);
}
}Agora Money funciona em qualquer lugar onde um Comparable é esperado — Collections.sort(...), TreeMap, Arrays.sort(...), seus próprios algoritmos genéricos. A biblioteca padrão e o seu código concordam com Comparable como uma abstração compartilhada; nenhum dos lados sabe nada sobre o outro.
A grande maioria das interfaces padrão do Java (List, Map, Iterable, Runnable, Function, Comparator, AutoCloseable) funciona dessa forma: um contrato pequeno e focado ao qual muitas classes concretas se conectam.
Abstração como alavanca de design
A parte mecânica da abstração — a palavra-chave abstract, a declaração interface — é pequena. A parte difícil é escolher quais abstrações definir. Três padrões que aparecem repetidamente:
- Strategy. Defina uma interface para "o algoritmo." Diferentes implementações trocam o algoritmo sem alterar o código que o usa.
Comparatoré o clássico. - Template method. Uma classe abstrata implementa o fluxo geral, com métodos abstratos nos pontos de variação. As subclasses preenchem as etapas específicas. O método
servicedoHttpServleté um exemplo famoso. - Plugin / ponto de extensão. Uma biblioteca publica uma interface; o código do usuário a implementa; a biblioteca faz chamadas de retorno para ela. APIs Servlet, drivers JDBC,
BeanPostProcessordo Spring.
Em todos os casos, o ganho é o mesmo: o código que depende da abstração é fechado contra mudanças nas implementações e aberto para que implementações adicionais sejam adicionadas posteriormente.
Encapsulamento vs abstração
Esses dois são primos próximos e frequentemente se confundem.
- Encapsulamento oculta a implementação de uma classe específica (campos privados, métodos controlados). É uma preocupação interna da classe.
- Abstração oculta qual classe você está usando por trás de um contrato compartilhado. É uma preocupação externa à classe.
Uma classe com campos private e uma API pública organizada é encapsulada, mas ainda não é abstraída — os chamadores ainda dependem dessa classe específica. Substitua o tipo na fronteira da API por uma interface, e os chamadores dependem do contrato em vez disso. Agora você pode trocar as implementações.
Para vê-los funcionando juntos, veja o capítulo de encapsulamento: o encapsulamento bloqueia uma única classe, a abstração permite que os chamadores ignorem qual classe eles têm.
Erros comuns
Algumas armadilhas pegam os recém-chegados à abstração:
- Tentar instanciar um tipo abstrato.
new Shape()é um erro de compilação quandoShapeéabstractou uma interface. Você instancia um subtipo concreto (new Circle(2)) e o atribui à referência abstrata. - Abstrair cedo demais. Uma interface com exatamente uma implementação, escrita "caso precisemos de outra mais tarde," geralmente é peso morto. Adicione a abstração quando a segunda implementação realmente aparecer, ou quando você genuinamente precisar desacoplar dois módulos. A abstração prematura adiciona indireção sem ganhar flexibilidade.
- Vazar o tipo concreto. Declarar um campo ou parâmetro como
ArrayListem vez deList, ou retornarHashMapem vez deMap, vincula os chamadores a essa classe específica e desfaz a abstração. Prefira o tipo mais abstrato que ainda expressa o que você precisa. - Confundir "sem corpo" com "não faz nada." Um método abstrato não tem corpo porque as subclasses devem fornecer um. Um método concreto com um corpo vazio é um método real que não faz nada — um contrato muito diferente.
Um exemplo prático
Execute o programa abaixo. Ele exercita ambos os mecanismos: uma classe abstrata Shape com código compartilhado describe, e uma interface pura Greeter. A saída esperada é:
Circle area=12.566370614359172
Square area=9.0
total = 21.57
Dear Alice,
hey Alice!Observe que totalArea e o laço do greeter nunca nomeiam Circle, Square, FormalGreeter ou CasualGreeter — eles falam apenas com as abstrações Shape e Greeter.
O que vem a seguir
O próximo capítulo trata da mecânica concreta das classes abstratas — métodos abstratos, o que eles permitem que uma subclasse herde, quando escolhê-las em vez de interfaces.