Java Essencial: Classes e objetos na prática (estado, comportamento e encapsulamento)

Capítulo 10

Tempo estimado de leitura: 9 minutos

+ Exercício

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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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

  • this referencia 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.: reajustarPreco em vez de setPreco).
  • Se um campo não deve mudar após criação, considere torná-lo final e não criar setter (ex.: nome do 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

ClasseResponsabilidadeExemplos de regras
ProdutoRepresentar um produto vendávelPreço não negativo; estoque não negativo
ItemPedidoRepresentar um produto + quantidade no pedidoQuantidade > 0; subtotal calculado
PedidoGerenciar itens e total do pedidoNã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, adicionarItem em vez de setSaldo, 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.

Agora responda o exercício sobre o conteúdo:

Qual alternativa descreve melhor uma prática de encapsulamento para manter as invariantes de um objeto (como estoque e preço) consistentes?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Encapsular é proteger o estado (campos private) e expor operações seguras. Construtores e métodos com intenção validam regras (invariantes), evitando alterações diretas ou setters genéricos que podem gerar inconsistências.

Próximo capitúlo

Java Essencial: Strings, imutabilidade e manipulação eficiente

Arrow Right Icon
Capa do Ebook gratuito Java Essencial: Fundamentos da Linguagem e do Ecossistema (JDK, IDE, Maven)
56%

Java Essencial: Fundamentos da Linguagem e do Ecossistema (JDK, IDE, Maven)

Novo curso

18 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.