Estado e comportamento: o que uma classe representa
Em Java, uma classe define um “molde” para criar objetos. Um objeto combina estado (dados) e comportamento (ações). O estado costuma ser representado por atributos (campos), e o comportamento por métodos de instância (métodos que atuam sobre um objeto específico).
Uma boa modelagem orientada a objetos tenta responder: “Quais dados esse objeto precisa guardar?” e “Quais ações fazem sentido esse objeto executar?”. Isso ajuda a evitar classes que apenas guardam dados sem regras (classes “anêmicas”).
Atributos (campos) e visibilidade
Campos devem, em geral, ser private para proteger o estado interno. Assim, o objeto controla como seu estado muda, mantendo regras (invariantes) consistentes.
public class Produto { private String nome; private int quantidadeEmEstoque; private double preco;}Com private, ninguém de fora altera diretamente preco ou quantidadeEmEstoque sem passar pelas regras do objeto.
Construtores e o uso de this
O construtor define como um objeto nasce. É o melhor lugar para garantir que o objeto seja criado em um estado válido.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Construtor com validação de invariantes
Invariantes são regras que devem ser verdadeiras durante toda a vida do objeto (por exemplo: preço não pode ser negativo; saldo não pode ficar incoerente; quantidade em estoque não pode ser negativa).
public class Produto { private final String nome; private int quantidadeEmEstoque; private double preco; public Produto(String nome, int quantidadeEmEstoque, double preco) { if (nome == null || nome.isBlank()) { throw new IllegalArgumentException("Nome é obrigatório"); } if (quantidadeEmEstoque < 0) { throw new IllegalArgumentException("Quantidade não pode ser negativa"); } if (preco < 0) { throw new IllegalArgumentException("Preço não pode ser negativo"); } this.nome = nome; this.quantidadeEmEstoque = quantidadeEmEstoque; this.preco = preco; }}O que é this na prática
thisreferencia o objeto atual (a instância em que o método/constructor está executando).- É útil para diferenciar campo de parâmetro com o mesmo nome:
this.preco = preco. - Também pode ser usado para chamar outro construtor da mesma classe:
this(...)(encadeamento de construtores).
public Produto(String nome, double preco) { this(nome, 0, preco);}Métodos de instância: comportamento com regras
Em vez de expor campos e permitir alterações livres, prefira métodos que expressem intenções do domínio: adicionarEstoque, baixarEstoque, reajustarPreco. Isso reduz “setters sem regra” e concentra a responsabilidade no objeto.
public class Produto { private final String nome; private int quantidadeEmEstoque; private double preco; public Produto(String nome, int quantidadeEmEstoque, double preco) { if (nome == null || nome.isBlank()) throw new IllegalArgumentException("Nome é obrigatório"); if (quantidadeEmEstoque < 0) throw new IllegalArgumentException("Quantidade não pode ser negativa"); if (preco < 0) throw new IllegalArgumentException("Preço não pode ser negativo"); this.nome = nome; this.quantidadeEmEstoque = quantidadeEmEstoque; this.preco = preco; } public void adicionarEstoque(int quantidade) { if (quantidade <= 0) { throw new IllegalArgumentException("Quantidade deve ser positiva"); } this.quantidadeEmEstoque += quantidade; } public void baixarEstoque(int quantidade) { if (quantidade <= 0) { throw new IllegalArgumentException("Quantidade deve ser positiva"); } if (quantidade > this.quantidadeEmEstoque) { throw new IllegalStateException("Estoque insuficiente"); } this.quantidadeEmEstoque -= quantidade; } public void reajustarPreco(double novoPreco) { if (novoPreco < 0) { throw new IllegalArgumentException("Preço não pode ser negativo"); } this.preco = novoPreco; }}Getters e setters: quando usar (e quando evitar)
Getters expõem leitura do estado. Setters expõem escrita. O problema de “excesso de setters” é permitir que qualquer parte do sistema altere o objeto sem respeitar regras, espalhando validações e criando inconsistências.
Boas práticas
- Use getters para leitura necessária (ex.: exibir dados, calcular relatórios).
- Evite setters genéricos para campos que têm regra. Prefira métodos com intenção (ex.:
reajustarPrecoem vez desetPreco). - Se um campo não deve mudar após criação, considere torná-lo
finale não criar setter (ex.:nomedo produto).
public String getNome() { return nome;}public int getQuantidadeEmEstoque() { return quantidadeEmEstoque;}public double getPreco() { return preco;}Validação centralizada
Uma técnica comum é criar métodos privados para validar regras e reutilizá-los em construtores e métodos públicos.
private static void validarPreco(double preco) { if (preco < 0) throw new IllegalArgumentException("Preço não pode ser negativo");}Encapsulamento e responsabilidades
Encapsulamento é esconder detalhes internos e expor apenas operações seguras. O objetivo não é “esconder por esconder”, mas garantir que o objeto preserve suas invariantes e que o código que usa o objeto fique mais simples.
Exemplo de classe anêmica (evitar)
public class Conta { public double saldo;}Se saldo é público, qualquer código pode fazer conta.saldo = -1000, quebrando regras. Mesmo com setters, se eles não validam, o problema continua.
Exemplo com responsabilidade bem definida
public class Conta { private final String titular; private double saldo; public Conta(String titular, double saldoInicial) { if (titular == null || titular.isBlank()) { throw new IllegalArgumentException("Titular é obrigatório"); } if (saldoInicial < 0) { throw new IllegalArgumentException("Saldo inicial não pode ser negativo"); } this.titular = titular; this.saldo = saldoInicial; } public void depositar(double valor) { if (valor <= 0) throw new IllegalArgumentException("Depósito deve ser positivo"); this.saldo += valor; } public void sacar(double valor) { if (valor <= 0) throw new IllegalArgumentException("Saque deve ser positivo"); if (valor > this.saldo) throw new IllegalStateException("Saldo insuficiente"); this.saldo -= valor; } public String getTitular() { return titular; } public double getSaldo() { return saldo; }}Mini-projeto OO: Pedido com Produto e Item
Neste mini-projeto, você vai criar um pequeno modelo com três classes: Produto, ItemPedido e Pedido. O objetivo é praticar: criação de objetos, atualização de estado com regras e impressão de dados.
Visão geral das responsabilidades
| Classe | Responsabilidade | Exemplos de regras |
|---|---|---|
Produto | Representar um produto vendável | Preço não negativo; estoque não negativo |
ItemPedido | Representar um produto + quantidade no pedido | Quantidade > 0; subtotal calculado |
Pedido | Gerenciar itens e total do pedido | Não adicionar item com estoque insuficiente |
Passo 1: criar a classe Produto
public class Produto { private final String sku; private final String nome; private int estoque; private double preco; public Produto(String sku, String nome, int estoque, double preco) { if (sku == null || sku.isBlank()) throw new IllegalArgumentException("SKU é obrigatório"); if (nome == null || nome.isBlank()) throw new IllegalArgumentException("Nome é obrigatório"); if (estoque < 0) throw new IllegalArgumentException("Estoque não pode ser negativo"); if (preco < 0) throw new IllegalArgumentException("Preço não pode ser negativo"); this.sku = sku; this.nome = nome; this.estoque = estoque; this.preco = preco; } public void baixarEstoque(int quantidade) { if (quantidade <= 0) throw new IllegalArgumentException("Quantidade deve ser positiva"); if (quantidade > estoque) throw new IllegalStateException("Estoque insuficiente"); this.estoque -= quantidade; } public void adicionarEstoque(int quantidade) { if (quantidade <= 0) throw new IllegalArgumentException("Quantidade deve ser positiva"); this.estoque += quantidade; } public void reajustarPreco(double novoPreco) { if (novoPreco < 0) throw new IllegalArgumentException("Preço não pode ser negativo"); this.preco = novoPreco; } public String getSku() { return sku; } public String getNome() { return nome; } public int getEstoque() { return estoque; } public double getPreco() { return preco; }}Passo 2: criar a classe ItemPedido
O item guarda o produto e a quantidade. O subtotal é comportamento: depende do estado atual do item.
public class ItemPedido { private final Produto produto; private int quantidade; public ItemPedido(Produto produto, int quantidade) { if (produto == null) throw new IllegalArgumentException("Produto é obrigatório"); if (quantidade <= 0) throw new IllegalArgumentException("Quantidade deve ser positiva"); this.produto = produto; this.quantidade = quantidade; } public void aumentarQuantidade(int adicional) { if (adicional <= 0) throw new IllegalArgumentException("Adicional deve ser positivo"); this.quantidade += adicional; } public double getSubtotal() { return produto.getPreco() * quantidade; } public Produto getProduto() { return produto; } public int getQuantidade() { return quantidade; }}Passo 3: criar a classe Pedido
O pedido gerencia itens e aplica regras de adição. Note que o pedido não expõe uma lista mutável diretamente; ele oferece métodos para operar com segurança.
import java.util.ArrayList;import java.util.List;public class Pedido { private final String numero; private final List<ItemPedido> itens = new ArrayList<>(); public Pedido(String numero) { if (numero == null || numero.isBlank()) { throw new IllegalArgumentException("Número do pedido é obrigatório"); } this.numero = numero; } public void adicionarItem(Produto produto, int quantidade) { if (produto == null) throw new IllegalArgumentException("Produto é obrigatório"); if (quantidade <= 0) throw new IllegalArgumentException("Quantidade deve ser positiva"); if (quantidade > produto.getEstoque()) { throw new IllegalStateException("Estoque insuficiente para o produto: " + produto.getSku()); } produto.baixarEstoque(quantidade); itens.add(new ItemPedido(produto, quantidade)); } public double getTotal() { double total = 0; for (ItemPedido item : itens) { total += item.getSubtotal(); } return total; } public String gerarResumo() { StringBuilder sb = new StringBuilder(); sb.append("Pedido ").append(numero).append("\n"); for (ItemPedido item : itens) { sb.append("- ") .append(item.getProduto().getNome()) .append(" (").append(item.getProduto().getSku()).append(")") .append(" x").append(item.getQuantidade()) .append(" = ").append(item.getSubtotal()) .append("\n"); } sb.append("Total: ").append(getTotal()); return sb.toString(); } public String getNumero() { return numero; } public List<ItemPedido> getItensSomenteLeitura() { return List.copyOf(itens); }}Passo 4: criar, atualizar e imprimir dados (classe de execução)
Este exemplo cria produtos, ajusta preço/estoque, cria um pedido, adiciona itens e imprime um resumo. Repare que as mudanças de estado acontecem por métodos com regra (não por setters genéricos).
public class AppPedidos { public static void main(String[] args) { Produto cafe = new Produto("SKU-CAFE", "Café 500g", 10, 18.90); Produto leite = new Produto("SKU-LEITE", "Leite 1L", 20, 5.49); cafe.reajustarPreco(19.90); leite.adicionarEstoque(5); Pedido pedido = new Pedido("PED-1001"); pedido.adicionarItem(cafe, 2); pedido.adicionarItem(leite, 6); System.out.println(pedido.gerarResumo()); System.out.println("Estoque restante do café: " + cafe.getEstoque()); System.out.println("Estoque restante do leite: " + leite.getEstoque()); }}Checklist de design: evitando setters sem regra
- Campos sensíveis privados: saldo, estoque, preço, status, limites.
- Construtor garante estado válido: não crie objetos “meio prontos”.
- Métodos com intenção:
depositar,baixarEstoque,adicionarItemem vez desetSaldo,setEstoque,setItens. - Getters com parcimônia: exponha o necessário; evite expor coleções mutáveis diretamente.
- Invariantes centralizadas: valide sempre que o estado puder mudar.