W3docs

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:

  1. O que os chamadores podem fazer com valores desse tipo? (sua API)
  2. 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

MecanismoO que forneceQuando usar
abstract classUma classe parcial — alguns métodos abstratos, outros com corpos, além de campos e construtoresQuando os subtipos compartilharão estado e código de infraestrutura
interfaceUm contrato puro (ou quase puro) — métodos que as classes implementadoras devem fornecer; sem estado de instânciaQuando 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 service do HttpServlet é 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, BeanPostProcessor do 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 quando Shape é abstract ou 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 ArrayList em vez de List, ou retornar HashMap em vez de Map, 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.

java— editable, runs on the server

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.

Prática

Prática
Qual melhor captura a diferença entre encapsulamento e abstração?
Qual melhor captura a diferença entre encapsulamento e abstração?
Prática
Quando você deve usar uma interface em vez de uma classe abstrata?
Quando você deve usar uma interface em vez de uma classe abstrata?
Was this page helpful?