O que são classes e objetos (na prática)
Em Java, classe é um molde que descreve um tipo de coisa do domínio: quais dados ela guarda (atributos/campos) e o que ela sabe fazer (comportamentos/métodos). Um objeto é uma instância concreta criada a partir da classe, com valores próprios para seus campos.
Uma forma útil de pensar: a classe define responsabilidades. Se uma classe tem responsabilidades bem definidas, ela tende a ser mais fácil de entender, testar e evoluir.
Responsabilidade, coesão e dependências
- Responsabilidade: aquilo que a classe deve fazer (e apenas isso).
- Coesão (cohesion): quão “unidas” são as responsabilidades dentro da classe. Alta coesão significa que os métodos e campos da classe trabalham juntos para um objetivo claro.
- Dependências: outras classes/serviços que uma classe precisa para cumprir seu papel. Dependências em excesso ou mal posicionadas aumentam acoplamento e dificultam mudanças.
Regra prática: se você descreve uma classe com “e” demais (ex.: “faz X e Y e Z”), provavelmente ela está assumindo responsabilidades demais.
Modelando um domínio simples: Biblioteca
Vamos modelar um domínio pequeno de biblioteca com foco em identificar entidades, atributos e comportamentos. Requisitos iniciais (simples e realistas):
- Cadastrar livros com título, autor e quantidade de exemplares disponíveis.
- Emprestar um livro (reduz disponibilidade) e devolver (aumenta).
- Não permitir emprestar quando não há exemplares.
- Gerar um identificador simples para cada livro.
Passo 1: identificar entidades
Entidades candidatas:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
- Livro: representa um item do acervo e controla sua disponibilidade.
- (Opcional em versões futuras) Usuário, Empréstimo, Biblioteca/Catálogo.
Como estamos no começo, vamos começar com Livro e um pequeno programa para exercitar a classe.
Passo 2: listar atributos (estado)
id: identificador do livro.tituloautorexemplaresDisponiveis
Passo 3: listar comportamentos (métodos)
emprestar(): tenta emprestar; se não houver exemplares, falha.devolver(): devolve um exemplar.getterspara leitura (evitar expor campos diretamente).
Implementando as primeiras classes em Java
Campos, construtores e encapsulamento
Encapsulamento é manter os campos como private e expor operações seguras via métodos. Isso permite validar regras (ex.: não deixar disponibilidade negativa).
public class Livro { private final String id; private final String titulo; private final String autor; private int exemplaresDisponiveis; public Livro(String id, String titulo, String autor, int exemplaresDisponiveis) { if (id == null || id.isBlank()) { throw new IllegalArgumentException("id obrigatório"); } if (titulo == null || titulo.isBlank()) { throw new IllegalArgumentException("título obrigatório"); } if (autor == null || autor.isBlank()) { throw new IllegalArgumentException("autor obrigatório"); } if (exemplaresDisponiveis < 0) { throw new IllegalArgumentException("exemplaresDisponiveis não pode ser negativo"); } this.id = id; this.titulo = titulo; this.autor = autor; this.exemplaresDisponiveis = exemplaresDisponiveis; } public String getId() { return id; } public String getTitulo() { return titulo; } public String getAutor() { return autor; } public int getExemplaresDisponiveis() { return exemplaresDisponiveis; } public boolean emprestar() { if (exemplaresDisponiveis <= 0) { return false; } exemplaresDisponiveis--; return true; } public void devolver() { exemplaresDisponiveis++; }}Observações importantes:
finalem campos indica que o valor não muda após o construtor (bom para identidade e dados imutáveis).- O método
emprestar()retornabooleanpara indicar sucesso/falha sem lançar exceção em um fluxo esperado. - O método
devolver()é simples; em um sistema real, você poderia validar limites, histórico etc.
Instanciando objetos e chamando métodos
public class ProgramaBiblioteca { public static void main(String[] args) { Livro livro = new Livro("L-001", "Java para Domínios", "Ana Silva", 2); System.out.println(livro.getTitulo() + " - disponíveis: " + livro.getExemplaresDisponiveis()); boolean ok1 = livro.emprestar(); boolean ok2 = livro.emprestar(); boolean ok3 = livro.emprestar(); System.out.println("Empréstimo 1: " + ok1); System.out.println("Empréstimo 2: " + ok2); System.out.println("Empréstimo 3: " + ok3); System.out.println("Disponíveis agora: " + livro.getExemplaresDisponiveis()); livro.devolver(); System.out.println("Disponíveis após devolução: " + livro.getExemplaresDisponiveis()); }}Esse exemplo já mostra o essencial: criar objeto com new, inicializar via construtor e usar métodos para alterar o estado com regras.
Membros static e constantes: quando usar
static significa “pertence à classe, não ao objeto”. Use quando a informação/ação é compartilhada por todas as instâncias ou quando você precisa de um utilitário sem estado de instância.
Constantes com static final
Constantes são valores fixos, normalmente public static final. Exemplo: um prefixo padrão de ID.
public class Livro { public static final String PREFIXO_ID = "L-"; private static int sequencia = 1; private final String id; private final String titulo; private final String autor; private int exemplaresDisponiveis; public Livro(String titulo, String autor, int exemplaresDisponiveis) { this(gerarId(), titulo, autor, exemplaresDisponiveis); } public Livro(String id, String titulo, String autor, int exemplaresDisponiveis) { if (id == null || id.isBlank()) { throw new IllegalArgumentException("id obrigatório"); } if (titulo == null || titulo.isBlank()) { throw new IllegalArgumentException("título obrigatório"); } if (autor == null || autor.isBlank()) { throw new IllegalArgumentException("autor obrigatório"); } if (exemplaresDisponiveis < 0) { throw new IllegalArgumentException("exemplaresDisponiveis não pode ser negativo"); } this.id = id; this.titulo = titulo; this.autor = autor; this.exemplaresDisponiveis = exemplaresDisponiveis; } private static String gerarId() { return PREFIXO_ID + (sequencia++); } public String getId() { return id; } public String getTitulo() { return titulo; } public String getAutor() { return autor; } public int getExemplaresDisponiveis() { return exemplaresDisponiveis; } public boolean emprestar() { if (exemplaresDisponiveis <= 0) { return false; } exemplaresDisponiveis--; return true; } public void devolver() { exemplaresDisponiveis++; }}Pontos de atenção:
sequenciaéstaticporque é compartilhada por todos os livros para gerar IDs.gerarId()éstaticporque não depende de um objeto já existente.- Esse gerador é simples e serve para exercício; em aplicações reais, IDs podem vir de banco, UUID etc.
Transformando requisitos em classes: roteiro guiado
Checklist de modelagem (rápido e prático)
| Pergunta | O que você produz | Exemplo (Biblioteca) |
|---|---|---|
| Quais são os “substantivos” do domínio? | Entidades/classes candidatas | Livro, Usuário, Empréstimo |
| O que cada entidade precisa lembrar? | Atributos/campos | título, autor, disponíveis |
| O que cada entidade precisa fazer? | Métodos | emprestar, devolver |
| Quais regras não podem ser quebradas? | Validações e invariantes | disponíveis nunca negativo |
| Quem deve conhecer quem? | Dependências | Livro não precisa conhecer Usuário (ainda) |
Exercício guiado 1: do requisito à classe
Requisito: “Uma conta bancária tem número, titular e saldo. Deve permitir depositar e sacar. Não permitir saque maior que o saldo.”
Passo a passo:
- Entidade:
ContaBancaria - Atributos:
numero,titular,saldo - Métodos:
depositar(valor),sacar(valor), getters - Invariantes:
saldo >= 0;valor > 0
public class ContaBancaria { private final String numero; private final String titular; private double saldo; public ContaBancaria(String numero, String titular, double saldoInicial) { if (numero == null || numero.isBlank()) { throw new IllegalArgumentException("número obrigatório"); } if (titular == null || titular.isBlank()) { throw new IllegalArgumentException("titular obrigatório"); } if (saldoInicial < 0) { throw new IllegalArgumentException("saldoInicial não pode ser negativo"); } this.numero = numero; this.titular = titular; this.saldo = saldoInicial; } public String getNumero() { return numero; } public String getTitular() { return titular; } public double getSaldo() { return saldo; } public void depositar(double valor) { if (valor <= 0) { throw new IllegalArgumentException("valor de depósito deve ser positivo"); } saldo += valor; } public boolean sacar(double valor) { if (valor <= 0) { throw new IllegalArgumentException("valor de saque deve ser positivo"); } if (valor > saldo) { return false; } saldo -= valor; return true; }}Validação de responsabilidade: a classe cuida do saldo e das regras de movimentação. Ela não imprime extrato, não salva em arquivo, não acessa banco. Isso mantém alta coesão.
Exercício guiado 2: melhorando coesão (separando responsabilidades)
Cenário: você criou uma classe Biblioteca que “cadastra livro, empresta, devolve, imprime relatórios e também lê dados do teclado”.
Problema: responsabilidades demais (baixa coesão). Misturar regras de domínio com entrada/saída (I/O) torna o código difícil de testar.
Tarefa: separe em pelo menos duas classes:
Biblioteca: regras de negócio (cadastrar, emprestar, devolver).RelatorioBiblioteca(ouFormatador): apenas formatação/relatório.- (Opcional)
ProgramaBiblioteca: interação com usuário (main).
Esqueleto sugerido:
import java.util.ArrayList;import java.util.List;public class Biblioteca { private final List<Livro> acervo = new ArrayList<>(); public void cadastrar(Livro livro) { if (livro == null) { throw new IllegalArgumentException("livro obrigatório"); } acervo.add(livro); } public List<Livro> getAcervo() { return List.copyOf(acervo); } public boolean emprestarPorId(String id) { Livro livro = encontrarPorId(id); if (livro == null) return false; return livro.emprestar(); } public boolean devolverPorId(String id) { Livro livro = encontrarPorId(id); if (livro == null) return false; livro.devolver(); return true; } private Livro encontrarPorId(String id) { for (Livro l : acervo) { if (l.getId().equals(id)) { return l; } } return null; }}public class RelatorioBiblioteca { public String gerarResumo(Biblioteca biblioteca) { StringBuilder sb = new StringBuilder(); for (Livro l : biblioteca.getAcervo()) { sb.append(l.getId()) .append(" | ") .append(l.getTitulo()) .append(" | ") .append(l.getAutor()) .append(" | disponíveis: ") .append(l.getExemplaresDisponiveis()) .append("\n"); } return sb.toString(); }}Checagem de dependências: RelatorioBiblioteca depende de Biblioteca e Livro para ler dados, mas não altera regras. Biblioteca depende de Livro (faz sentido), mas não depende de relatório nem de console.
Exercício guiado 3: identificando static mal utilizado
Requisito: “Cada livro tem sua própria disponibilidade.”
Erro comum: declarar exemplaresDisponiveis como static, fazendo com que todos os livros compartilhem o mesmo valor.
Tarefa: explique por que exemplaresDisponiveis deve ser campo de instância (sem static) e cite um exemplo de campo que faz sentido ser static no domínio (por exemplo, um gerador de sequência, uma constante de prefixo, ou uma política global).
Prática orientada: mini-desafio de modelagem (Pedidos)
Requisitos:
- Um pedido tem número e itens.
- Um item tem descrição, quantidade e preço unitário.
- O pedido calcula o total (soma de quantidade * preço).
- Não permitir quantidade menor ou igual a zero, nem preço negativo.
Tarefa 1: liste as classes e responsabilidades (duas classes são suficientes).
Tarefa 2: implemente:
ItemPedidocom validações no construtor.Pedidocom uma lista de itens, métodoadicionarIteme métodogetTotal().
Tarefa 3 (cohesion): decida onde deve ficar o cálculo do subtotal do item: em ItemPedido (ex.: getSubtotal()) ou em Pedido. Justifique com base em responsabilidade e coesão.