04 / 11 Curso de Java do zero | fundamentos e linguagem · Aula escrita · atualizado em 13 out 2026

Quem impede um estoque de ficar negativo?

Imagine que seu programa tenha quantidade = 3. Uma tela subtrai dois, outra subtrai cinco e o estoque termina negativo. Colocar um if em cada tela resolve somente enquanto ninguém esquece. Podemos colocar a regra junto do estado que ela protege.

Fernanda Kipper
Fernanda Kipper
Próxima aula →

Imagine que seu programa tenha quantidade = 3. Uma tela subtrai dois, outra subtrai cinco e o estoque termina negativo. Colocar um if em cada tela resolve somente enquanto ninguém esquece. Podemos colocar a regra junto do estado que ela protege.

É essa a utilidade central de um objeto neste exemplo: manter dados e oferecer operações que preservem suas regras. Você já deve conhecer variáveis, métodos e condições. Usaremos Java 21, sem framework. Ao final, você conseguirá criar um objeto válido, modificar seu estado por operações explícitas e trocar uma implementação sem mudar a regra principal.

Classe descreve; objeto existe durante a execução

Uma classe define atributos e métodos. Um objeto é uma instância dessa classe, com seu estado durante a execução. Um atributo de instância pertence àquela instância; um atributo static pertence à classe. Um contador estático compartilhado por todos os estoques não representaria corretamente a quantidade de cada produto.

O construtor participa da criação e permite exigir condições iniciais. private restringe acesso direto ao estado. Isso é parte do encapsulamento: quem usa o objeto deve depender de suas operações, não de como ele guarda tudo por dentro.

Salve este programa em EstoqueDemo.java:

public class EstoqueDemo {
    static final class Estoque {
        private int disponivel;

        Estoque(int inicial) {
            if (inicial < 0) {
                throw new IllegalArgumentException("Estoque inicial negativo");
            }
            this.disponivel = inicial;
        }

        void reservar(int quantidade) {
            if (quantidade <= 0) {
                throw new IllegalArgumentException("Reserva deve ser positiva");
            }
            if (quantidade > disponivel) {
                throw new IllegalStateException("Estoque insuficiente");
            }
            disponivel -= quantidade;
        }

        int disponivel() {
            return disponivel;
        }
    }

    public static void main(String[] args) {
        Estoque livro = new Estoque(3);
        livro.reservar(2);
        System.out.println(livro.disponivel());
        try {
            livro.reservar(5);
        } catch (IllegalStateException erro) {
            System.out.println(erro.getMessage());
        }
        System.out.println(livro.disponivel());
    }
}

A saída é 1, Estoque insuficiente, 1. O pedido inválido não altera o estado porque as verificações acontecem antes da subtração. Chamamos essa propriedade de invariante: uma condição que deve continuar verdadeira quando uma operação pública termina normalmente. Aqui, a quantidade disponível nunca é negativa.

this.disponivel se refere ao atributo do objeto atual. A classe interna é static para não depender de uma instância de EstoqueDemo; final na declaração da classe impede subclasses. Usamos essa organização para manter um exemplo inteiro em um arquivo, não como obrigação para todos os projetos.

Por que não adicionar um setter para tudo?

Um método setDisponivel(-100) permitiria violar a regra. Poderíamos validar esse setter, mas ainda perderíamos a intenção: o chamador está corrigindo um inventário, devolvendo um item ou fazendo uma reserva? Métodos com nomes do domínio ajudam a explicitar operações e suas condições.

Um getter também pode expor demais. Se um objeto devolve sua lista interna mutável, outro código pode modificar o estado sem passar pelas verificações. Uma cópia ou visão não modificável pode proteger a estrutura, mas os elementos também precisam ser considerados: proteger a lista não torna automaticamente cada objeto imutável.

Uma interface representa o que a regra precisa

Agora queremos avisar quando uma reserva acontece. Se a regra criar diretamente um cliente de e-mail, ela ficará ligada àquele mecanismo. Uma interface declara um contrato: quais operações uma implementação deve oferecer.

Este trecho é independente do primeiro exemplo; salve em AvisoDemo.java:

public class AvisoDemo {
    interface Notificador {
        void enviar(String mensagem);
    }

    static final class Console implements Notificador {
        @Override
        public void enviar(String mensagem) {
            System.out.println("AVISO: " + mensagem);
        }
    }

    static final class Inscricao {
        private final Notificador notificador;

        Inscricao(Notificador notificador) {
            this.notificador = java.util.Objects.requireNonNull(notificador);
        }

        void confirmar() {
            notificador.enviar("Inscrição confirmada");
        }
    }

    public static void main(String[] args) {
        Inscricao inscricao = new Inscricao(new Console());
        inscricao.confirmar();
    }
}

A saída é AVISO: Inscrição confirmada. Inscricao recebe sua dependência pelo construtor. Isso é injeção de dependência: alguém fornece o colaborador em vez de a classe escolher como criá-lo. Não exige Spring.

implements declara que Console cumpre a interface. A chamada acontece pelo tipo Notificador, mas a implementação executada pertence ao objeto fornecido. Esse comportamento é polimorfismo. @Override permite ao compilador conferir que o método realmente implementa ou sobrescreve uma declaração esperada.

A dependência é final: depois de inicializada, a referência não será substituída. Isso não prova que o objeto referenciado não muda por dentro.

Composição, herança e classe abstrata

Em composição, um objeto tem colaboradores: inscrição tem um notificador. Em herança, uma classe especializa outra e deve poder respeitar seu contrato. Não escolha herança somente para reaproveitar três linhas. Uma subclasse que lança erro em uma operação normal da classe base pode surpreender quem usa o tipo base.

Uma classe abstrata pode combinar métodos implementados, estado e métodos abstratos que subclasses precisam completar. Uma interface permite expressar capacidades sem exigir compartilhar o mesmo estado base. Java permite implementar várias interfaces, mas uma classe só estende uma classe diretamente.

Sobrecarga cria métodos com mesmo nome e parâmetros diferentes. Sobrescrita fornece uma implementação para um método herdado compatível. O primeiro depende das assinaturas disponíveis; o segundo participa do comportamento polimórfico. Os nomes parecidos não significam o mesmo mecanismo.

Identidade e valor são perguntas diferentes

Duas instâncias podem representar o mesmo código de produto. == compara identidade de referência; equals pode representar igualdade de valor. Ao sobrescrever equals, mantenha hashCode coerente: objetos iguais devem produzir o mesmo hash. Coleções baseadas em hash dependem desse contrato.

Um record serve para representar dados por componentes. Por exemplo, record Codigo(String valor) {} fornece acesso ao componente e igualdade baseada nele. Não é imutabilidade profunda: um record que contém uma lista mutável ainda permite que o conteúdo dessa lista mude. Você pode fazer cópias defensivas no construtor para proteger o que o domínio exige.

Um enum, como enum Situacao { ABERTA, CONCLUIDA }, representa um conjunto fechado de valores nomeados. Ajuda a evitar variações acidentais de texto, mas não implementa sozinho as transições válidas entre estados.

Pratique uma mudança de regra

No estoque inicial de três unidades, reserve uma, tente reservar zero e consulte a quantidade. O resultado final deve ser dois: reserva zero falha antes de alterar o objeto. Se ficou três ou zero, procure onde a mutação ocorreu.

Depois crie outra implementação de Notificador que apenas conte mensagens e forneça-a à inscrição. A classe Inscricao não deve mudar. A resposta correta preserva o contrato e muda somente a montagem dos objetos; isso mostra a vantagem concreta da composição.

Em uma revisão futura, explique por que private sozinho não garante um objeto correto e por que final não significa objeto imutável. Esses contratos vão reaparecer ao escolher chaves para mapas e ao deixar um container gerenciar seus objetos no Spring.

Referência usada: guia de classes, objetos, encapsulamento e interfaces.

Material complementar da Fernanda

Pratique com /aprender

Copie o comando e cole no chat para iniciar uma prática guiada:

/aprender Pratique encapsulamento em Java 21 com uma classe Estoque que não pode ficar negativa. Peça que eu identifique a regra, proponha uma operação e explique como impedir um estado inválido. Dê uma tarefa por vez e não revele a solução antes da minha tentativa.
A SEGUIR · AULA 05

O que fazer quando os dados não vêm como você esperava?

Aula escrita · Um arquivo de cobranças contém duas linhas válidas, uma data impossível e um valor escrito como texto. Somar tudo como se nada tivesse acontecido produz um resultado errado. Parar sem explicar a causa também não ajuda quem enviou o arquivo. Precisamos distinguir falha de entrada, falha de leitura e uma operação válida.

Ir para a aula 05 →