O Objeto como Máquina de Estados

Aula 2 — Programação Orientada a Objetos

Autor

Marcos M. Raimundo — Instituto de Computação, UNICAMP

Data de Publicação

30 de agosto de 2026

Slides Lista de aulas

1 Proposta da Aula

Na Aula 1, o construtor de Produto chamou setPreco() para validar o preço inicial. Deixamos uma pergunta em aberto: e se o preço vier negativo — o construtor deve “consertar” o valor silenciosamente, ou recusar a existência do objeto? Esta aula responde a essa pergunta formalizando uma ideia que já rondava a Aula 1: todo objeto bem projetado é a implementação física de uma máquina de estados finita, protegida por um construtor que age como guardião e por métodos que agem como transições validadas.

O roteiro, em quatro perguntas:

  1. O que muda quando paramos de ver uma classe como um “saco de variáveis com getters e setters” e passamos a vê-la como uma máquina de estados?
  2. O que é uma invariante, e por que ela é o verdadeiro motivo de existir do encapsulamento?
  3. Por que o construtor é matematicamente diferente dos outros métodos — e o que acontece quando ele falha em proteger essa diferença?
  4. Quando um objeto deve recusar uma operação lançando uma exceção, e como o Java distingue “argumento errado” de “momento errado”?

2 Do Saco de Variáveis ao Agente Ativo

Em C, uma struct agrupa dados, mas é inteiramente passiva — as funções que operam sobre ela vivem em outro lugar, e a responsabilidade de manter a consistência recai sobre quem escreve essas funções externas:

struct Pedido {
    int status;
};

// A funcao externa dita a regra
void pagar(struct Pedido* p) {
    p->status = 1; // Mutacao direta!
}

Em Java, a classe agrupa os dados e as funções que têm o direito de alterá-los. O objeto deixa de ser memória passiva e se torna um agente ativo: você não altera o estado diretamente, você faz um pedido, e o objeto decide se a transição é legal.

public class Pedido {
    private Estado status;

    // O objeto controla a propria mutacao
    public void pagar() {
        if (this.status.podePagar()) {
            this.status = Estado.PAGO;
        }
    }
}

Essa união indissociável entre dados e as regras que os governam é o que chamamos de encapsulamento forte.

O anti-padrão do Modelo Anêmico. Um dos maiores desserviços do ensino básico de Java é reduzir o encapsulamento a “deixar os atributos private” sem se importar com o resto. Se todo atributo private ganha automaticamente um getter e um setter cegos, a inteligência vazou de volta para fora da classe — é apenas uma struct glorificada:

public class PedidoAnemico {
    private String status;

    // Setter cego: permite qualquer absurdo!
    public void setStatus(String s) { this.status = s; }
    public String getStatus() { return this.status; }
}

// O erro ocorre externamente:
pedido.setStatus("ENTREGUE"); // E o pagamento?

A regra de ouro: se você precisa inspecionar os dados de um objeto (get) para decidir por fora e depois alterar seu estado (set), o design falhou. O objeto deve tomar as próprias decisões.

3 A Máquina de Estados Finita e o Objeto

Um Autômato Finito Determinístico (DFA) é um modelo matemático clássico da Ciência da Computação: um sistema com um número limitado de estados, que muda de um estado para outro através de transições disparadas por eventos, de forma determinística — para cada estado e cada evento, existe exatamente um próximo estado.

O mapeamento para Java é direto: os atributos privados (em conjunto) definem em qual estado o objeto se encontra; os métodos públicos são as únicas transições autorizadas. O encapsulamento, sob esta luz, ganha um propósito mais profundo do que “esconder”: ele impede que o código externo “teletransporte” o objeto para um estado ilegal, pulando as transições validadas.

Considere um Produto com três estados lógicos, definidos pelo seu estoque:

A invariante que a máquina inteira existe para proteger é simples e inegociável: \(\text{estoque} \ge 0\). O objeto nasce em CADASTRADO (via new Produto()), transita para DISPONÍVEL ao repor estoque, e só aceita vender() nesse estado. A defesa é feita em código, dentro do próprio método de transição:

public class Produto {
    private int estoque; // O Estado Oculto

    // A Transicao (Metodo)
    public void vender(int quantidade) {
        // 1. Verificacao da legalidade da transicao (Guarda da Invariante)
        if (quantidade <= 0) {
            throw new IllegalArgumentException("Quantidade deve ser positiva.");
        }
        if (this.estoque < quantidade) {
            throw new IllegalStateException("Transicao invalida: Estoque insuficiente.");
        }
        // 2. Execucao da mudanca de estado
        this.estoque -= quantidade;
    }
}

Se o estoque tem 2 unidades e uma falha no carrinho tenta vender(5), a máquina não processa a requisição e assume um estado impossível de \(-3\): o método interroga o estado atual antes de mudar qualquer byte, e aborta a operação — o padrão Fail-Fast, que voltará com mais rigor ainda no Bloco do construtor.

DicaSe os atributos de um objeto fossem todos public, a máquina de estados continuaria funcionando — só ficaria “mais rápida”? Por quê?

Escreva sua resposta e compare com um colega antes de avançar (2 min). Julgue também V ou F os quatro itens abaixo — a questão só conta se acertar os 4:

  • □ Os atributos privados materializam os “nós” do grafo de estados do objeto.
  • □ Os métodos públicos são os únicos gatilhos autorizados a processar uma mudança de estado.
  • □ Tornar o atributo estoque público tornaria as transições mais rápidas, sem custo arquitetural.
  • □ Um DFA bem definido garante que, para cada estado e evento, exista exatamente uma transição válida.

4 Invariantes: do Laço à Classe

O conceito de invariante não nasceu na Orientação a Objetos — vem da análise de algoritmos. Uma invariante de laço é uma propriedade que se mantém verdadeira antes da primeira iteração (inicialização), permanece verdadeira a cada volta (manutenção), e no fim do laço prova que o algoritmo alcançou seu objetivo (término). No Insertion Sort, por exemplo, a invariante é: “no início de cada iteração \(i\), o subvetor de índices \(0\) a \(i-1\) contém os elementos originais, já ordenados”. Essa garantia é o que permite provar a corretude do algoritmo sem precisar “torcer” para que dê certo no final.

A invariante de classe eleva essa mesma ideia da execução de um laço para toda a vida de um objeto na Heap: é uma regra que deve continuar verdadeira do nascimento (construtor) até qualquer ponto depois de qualquer método. Exemplos concretos:

  • ContaBancaria: “o saldo mais o limite de crédito aprovado deve ser sempre \(\ge 0\)”.
  • Triangulo: “a soma de dois lados deve ser sempre maior que o terceiro” (\(a+b>c\), e as outras duas permutações).
  • Carrinho: “o total deve ser sempre igual à soma dos preços dos itens” — uma propriedade derivada, que não deveria ter um setTotal(valor) — o total é resultado, não entrada.
public class Carrinho {
    private double total = 0.0; // Estado Oculto

    // A transicao oficial garante a Invariante automaticamente
    public void adicionarItem(Item i) {
        if (i != null && i.getPreco() > 0) {
            this.total += i.getPreco();
        }
    }
}

Se você expõe saldo como public numa ContaBancaria usada por 50 módulos diferentes, cada um dos 50 precisaria lembrar de checar o limite antes de alterar o saldo — e basta um esquecimento para quebrar a invariante e criar um risco financeiro real. Encapsulando o atributo, a regra existe em um único lugar: dentro do método que a protege.

5 O Construtor como Base da Indução

Para quem gosta de rigor matemático: a corretude de uma classe pode ser vista como uma prova por indução finita. Seja \(S\) o conjunto de todos os estados possíveis de um objeto, e \(V \subset S\) o subconjunto que respeita todas as invariantes.

  • Base (\(n=0\)): o construtor tem o dever de garantir que o objeto recém-criado \(o\) satisfaça \(o \in V\) desde o primeiro milissegundo de vida na Heap. Se os argumentos levariam a \(o \notin V\), o construtor deve falhar ativamente.
  • Passo indutivo (\(n \Rightarrow n+1\)): dado um objeto em estado válido \(s_n \in V\), qualquer método público invocado sobre ele deve obrigatoriamente resultar em \(s_{n+1} \in V\).

Se o construtor falha em estabelecer a base, todo o resto do sistema desmorona — os métodos assumem, sem verificar de novo, que o objeto já nasceu válido. Um construtor permissivo cria o que se chama de Objeto Zumbi: uma instância que ocupa espaço na Heap, mas cujos dados não fazem sentido, produzindo erros distantes e difíceis de rastrear.

public class Produto {
    private String nome;
    private double preco;

    /** Constructor: A Base da Inducao */
    public Produto(String nome, double preco) {
        // FAIL-FAST: Validacao agressiva das Invariantes Iniciais
        if (nome == null || nome.trim().isEmpty()) {
            throw new IllegalArgumentException("Nome obrigatorio.");
        }
        if (preco <= 0) {
            throw new IllegalArgumentException("Preco deve ser positivo.");
        }
        // Se sobreviveu as validacoes, o estado V nasce.
        this.nome = nome;
        this.preco = preco;
    }
}

Um erro comum de quem está aprendendo: “consertar” o dado ruim silenciosamente (if (preco < 0) this.preco = 0.0;). Isso é um erro de design grave — mascarar dados inválidos cria uma falsa sensação de segurança e esconde uma falha de validação que deveria ter sido pega antes, na interface ou na camada de serviço. O construtor deve ser um guardião honesto: se a entrada está errada, a responsabilidade é de quem tentou criar o objeto, e o sistema deve recusar imediatamente.

6 Engenharia de Métodos: CQS e Design by Contract

Depois que o construtor garante um nascimento íntegro, a responsabilidade de manter a máquina segura passa para os métodos. O princípio CQS (Command-Query Separation) exige que todo método público pertença a exatamente um destes dois papéis:

  • Comando (Command): executa uma transição e altera o estado observável. Deve retornar void. Ex.: pedido.cancelar().
  • Consulta (Query): devolve um dado ao chamador. É proibido ter efeito colateral — uma pergunta não pode alterar a resposta. Ex.: produto.getPrecoLiquido().

O perigo de misturar os dois: imagine verificarSaldo() que, a cada chamada, secretamente cobra R$ 0,50 de taxa. Um desenvolvedor que só queria mostrar o saldo na tela acaba drenando a conta do usuário sem querer. Separar Comandos de Consultas garante que perguntar é sempre uma operação segura e idempotente.

O Design by Contract formaliza essa mesma disciplina em termos de pré e pós-condições:

public void processarSaque(double valor) {
    // PRE-CONDICAO: Protegendo a transicao contra chamadas invalidas
    if (valor <= 0) throw new IllegalArgumentException("Valor negativo.");
    if (this.saldo < valor) throw new IllegalStateException("Saldo insuficiente.");

    this.saldo -= valor; // EXECUCAO DA TRANSICAO (Comando)

    // POS-CONDICAO e INVARIANTE (Garantida pelo design do codigo acima)
    assert this.saldo >= 0 : "Invariante violada apos o saque!";
}

Pré-condições são barreiras de entrada: se falham, a culpa é de quem chamou o método fora de hora ou com dado inválido. Pós-condições atestam que, cumpridas as pré-condições, o método devolve o objeto num estado onde todas as invariantes seguem válidas — se isso falhar, o bug é do autor da classe, não de quem a chamou.

DicaUm método getSaldoLiquido() calcula um imposto e, de quebra, atualiza um contador interno de “quantas vezes o saldo foi consultado”. Isso viola algum princípio desta aula?

Escreva sua resposta e compare com um colega antes de avançar (2 min). Julgue também V ou F os quatro itens abaixo — a questão só conta se acertar os 4:

  • □ Um Comando deve, em geral, retornar void, sinalizando que sua função é mutar o estado, não devolver dado.
  • □ Uma Consulta pode alterar o estado interno do objeto, desde que devolva o valor correto ao chamador.
  • □ Se uma pré-condição falha, a responsabilidade recai sobre quem chamou o método fora de hora ou com dado inválido.
  • □ Se uma pós-condição falha após pré-condições válidas, o bug é do autor da classe, não de quem a chamou.

7 Fail-Fast: Exceções como Defesa

Em C, uma função de saque devolveria 1 (sucesso) ou -1 (falha). O perigo estrutural: códigos de erro podem ser ignorados silenciosamente — se o chamador esquecer o if (resultado == -1), o programa segue rodando como se nada tivesse acontecido, e os dados começam a se corromper sem aviso.

Fail-Fast propõe o oposto: ao detectar uma pré-condição violada, o método lança uma exceção, interrompendo o fluxo imediatamente, na mesma linha da infração — impossível de ignorar.

Para blindar uma máquina de estados, você não precisa (ainda) criar suas próprias exceções — o kit padrão do Java já cobre os casos comuns:

  • IllegalArgumentException — a culpa é da carga: um argumento passado não faz sentido no domínio (ex.: idade negativa).
  • IllegalStateException — a culpa é do momento: o dado está correto, mas o objeto não está no estado certo para a operação (ex.: carrinho.finalizarCompra() num carrinho vazio).
  • NullPointerException — usada proativamente via Objects.requireNonNull(cliente, "..."), para recusar um null já na porta de entrada, em vez de deixar o erro estourar linhas abaixo.
public class Elevador {
    private boolean portaAberta = false;
    private boolean emMovimento = false;

    public void subir() {
        // 1. Protegendo contra transicao em Estado Invalido
        if (this.portaAberta) {
            throw new IllegalStateException("Nao pode subir com porta aberta.");
        }
        if (this.emMovimento) {
            throw new IllegalStateException("O elevador ja esta em movimento.");
        }
        // 2. Mutacao Segura (Comando)
        this.emMovimento = true;
    }
}

Essas três exceções são subclasses de RuntimeException (não-checadas) — não exigem throws na assinatura nem try-catch do lado de quem chama. A filosofia é: um erro de contrato (argumento ruim, chamada fora de hora) é bug de quem chamou, e bugs devem falhar de forma visível para serem corrigidos antes de produção, não silenciados com try-catch.

8 Identidade Física vs. Identidade Lógica

Se criarmos duas instâncias idênticas de Produto na Heap, elas são o mesmo objeto? Para a JVM, não — == compara endereços físicos. Para o negócio, muitas vezes sim: dois objetos com o mesmo SKU deveriam ser tratados como “o mesmo produto”.

Produto p1 = new Produto(1, "Teclado");
Produto p2 = new Produto(1, "Teclado");

System.out.println(p1 == p2);      // false (enderecos fisicos diferentes)
System.out.println(p1.equals(p2)); // false! (Java nao sabe que o ID 1 importa)

O equals() herdado de Object usa == por padrão — ele não conhece o seu domínio de negócio. Se seu sistema tenta usar um HashSet<Produto> para evitar duplicatas no carrinho, e dois Produto com o mesmo SKU foram instanciados em requisições diferentes, o Set vai aceitar os dois como distintos — e o cliente é cobrado duas vezes pelo mesmo item.

A correção: sobrescrever equals() para comparar o atributo que de fato define identidade de negócio (o SKU), e hashCode() em conjunto — nunca um sem o outro:

@Override
public boolean equals(Object o) {
    if (this == o) return true;                                  // 1. mesma identidade fisica?
    if (o == null || getClass() != o.getClass()) return false;    // 2. protecao de tipo
    Produto produto = (Produto) o;
    return this.sku == produto.sku;                               // 3. regra de negocio: SKU define identidade
}

@Override
public int hashCode() {
    return Objects.hash(sku); // o hash deve olhar APENAS para os campos do equals
}

Analogia do armazém: hashCode() é o número do corredor (busca rápida); equals() é a checagem da etiqueta quando você chega lá. Se equals() compara o SKU mas hashCode() usa outra coisa (a cor da embalagem, digamos), caixas iguais acabam em corredores diferentes — o sistema procura no corredor errado e afirma, silenciosamente, que o item não existe. É por isso que os dois métodos formam um contrato indissociável.

DicaPor que reescrever equals() sem reescrever hashCode() junto é mais perigoso do que não reescrever nenhum dos dois?

Escreva sua resposta e compare com um colega antes de avançar (2 min). Julgue também V ou F os quatro itens abaixo — a questão só conta se acertar os 4:

  • □ O operador == em Java compara sempre a identidade física — o endereço na Heap.
  • □ O equals() herdado de Object, sem sobrescrita, já entende qual atributo define a igualdade de negócio.
  • □ Se dois objetos são iguais por equals(), seus hashCode() devem obrigatoriamente coincidir.
  • □ Um HashSet pode aceitar “duplicatas” de negócio se equals() não foi sobrescrito para refletir a identidade lógica correta.

9 Conclusão

Voltando às quatro perguntas da abertura:

  1. Saco de variáveis vs. máquina de estados: uma classe deixa de ser um struct glorificado quando os métodos, não o mundo externo, decidem se uma mudança de estado é legal.
  2. Invariante e o motivo do encapsulamento: a invariante é a regra que nunca pode ser violada; o encapsulamento existe precisamente para que só o próprio objeto possa proteger essa regra.
  3. O construtor é diferente: ele é a base de uma indução — se falha em recusar dados ruins, todo o resto do sistema herda a corrupção sob a forma de “objetos zumbis”.
  4. Quando lançar exceção: IllegalArgumentException quando o dado está errado, IllegalStateException quando o momento está errado — e ambas devem interromper o fluxo imediatamente, não ser mascaradas.

Ponte para a Aula 3

Falamos da máquina de estados por dentro — invariantes, construtor, transições. A Aula 3 olha para as bordas dessa máquina: o contrato entre interface e implementação visto com mais rigor filosófico, a diferença entre o escopo de uma variável e o ciclo de vida de um objeto (o que já apareceu na Aula 1 com a Heap e o GC), e a identidade revisitada sob a ótica da imutabilidade.

10 Exercícios

10.1 Questões discursivas

  1. Numa auditoria de um sistema de pagamentos, encontrou-se a classe Transferencia com getters e setters cegos para todas as propriedades, sem lógica interna alguma. Usando a teoria de máquinas de estado e proteção de invariantes, explique por que o Modelo Anêmico é um risco inaceitável em software financeiro.

  2. Um estagiário validou salários no construtor de Funcionario assim: if (salario < 0) this.salario = 0;. Usando o conceito de Fail-Fast e a analogia da base da indução, critique esse mecanismo e reescreva o construtor de forma robusta.

  3. Dois usuários preenchem os mesmos dados num formulário e o sistema cria dois objetos Passaporte distintos, com dados idênticos. Explique por que p1.equals(p2) retorna false por padrão, e como corrigir esse comportamento para refletir a identidade lógica correta do domínio.

10.2 Questões de Verdadeiro/Falso

Cada bloco de 4 itens trata do mesmo tema. A questão só é considerada correta se todos os 4 itens forem julgados corretamente (deixar em branco tem penalidade de 20% da nota da questão).

NotaEstado, Comportamento e Identidade (revisitados)
  • □ Um objeto sem nenhum atributo (nenhum campo mutável) não possui Estado algum, já que não haveria nada para variar ao longo do tempo.
  • □ Usando reflection (java.lang.reflect), é possível alterar um atributo private diretamente, sem passar por nenhum método — o que mostra que “Comportamento como única via de mutação” é uma convenção de projeto, não uma barreira física imposta pela JVM.
  • □ Dois objetos com Estado idêntico são sempre a mesma Identidade física na Heap.
  • □ A Identidade é garantida pela JVM no momento em que o objeto é instanciado via new.
NotaStruct Passivo vs. Agente Ativo
  • □ Em C, uma struct agrupa dados e as funções que operam sobre eles na mesma unidade.
  • □ Se a função que manipula uma struct em C for declarada static no mesmo arquivo-fonte da struct, ela deixa de ser “externa” e a struct passa a ter o mesmo encapsulamento forte de uma classe Java.
  • □ Basta que os atributos de uma classe Java sejam private para que o objeto recuse automaticamente qualquer mutação que viole suas regras internas, mesmo que o método que os altera não contenha nenhuma verificação.
  • □ Uma classe cujos atributos são todos public, mas que também oferece métodos como pagar() com toda a validação de regras de negócio, ainda apresenta encapsulamento forte, desde que os programadores usem apenas esses métodos por convenção, e nunca acessem os atributos diretamente.
NotaO Modelo de Domínio Anêmico
  • □ Uma classe com atributos private, apenas getters e nenhum setter (os atributos só podem ser lidos, nunca alterados após a construção), ainda se qualifica como Modelo de Domínio Anêmico.
  • □ É considerado um design rico, pois maximiza a flexibilidade de quem consome a classe.
  • □ A classe ProdutoBuilder, cujos métodos internos (comNome(...), comPreco(...)) apenas acumulam valores sem nenhuma validação, e cuja validação real só ocorre no método final build(), sofre exatamente do mesmo problema estrutural do Modelo Anêmico.
  • □ O design correto prefere métodos verbais de intenção (pagar(), cancelar()) a setters genéricos.
NotaA Máquina de Estados Finita (DFA)
  • □ Um autômato com um número infinito de estados possíveis ainda seria, por definição, determinístico, desde que cada par (estado, evento) leve a exatamente um único próximo estado.
  • □ No DFA do Produto apresentado na aula, chamar vender(quantidade) com uma quantidade maior que o estoque, estando o produto em DISPONÍVEL, não corresponde a nenhuma transição definida no diagrama — por isso o método precisa lançar uma exceção em vez de deixar a máquina seguir para um estado inexistente.
  • □ Um DFA bem definido permite comportamentos “mágicos” e imprevisíveis em casos de borda.
  • □ O determinismo de um DFA facilita a prova formal da corretude de um sistema.
NotaA Máquina de Estados Encapsulada em Java
  • □ Um atributo declarado static numa classe Java não faz parte do “nó” de estado de nenhuma instância individual da máquina, porque seu valor é compartilhado por todos os objetos daquela classe, e não isolado por instância.
  • □ Uma classe cujos únicos métodos públicos, além do construtor, são getters (nenhum método além deles), ainda define uma máquina de estados capaz de alcançar mais de um estado depois de criado o objeto.
  • □ Uma classe cujo atributo private List<Item> itens é exposto só por um getter que devolve a referência direta à lista (return this.itens;), mantém o mesmo nível de proteção contra transições ilegais que um atributo private bem encapsulado, já que a lista nunca é declarada public.
  • □ Um método público restaurarEstado(EstadoSerializado dump), que copia todos os campos de um objeto externo dump diretamente para os atributos private do objeto, sem nenhuma validação, preserva a proteção do encapsulamento, mesmo aceitando dados vindos de fora do sistema (como um arquivo ou uma requisição de rede).
NotaInvariantes de Classe
  • □ Durante a execução do corpo de um método público — antes de chegar à instrução return —, a invariante de classe pode estar temporariamente violada, desde que seja restaurada até o momento em que o método devolve o controle para quem o chamou.
  • □ Uma propriedade derivada, como o total de um carrinho, dispensa proteção por invariante.
  • □ Numa ContaBancaria cujo atributo saldo é public, mas que hoje é manipulada por um único módulo em todo o sistema, o risco de quebrar a invariante desaparece, pois só existe um lugar de código que precisa lembrar de checar a regra.
  • □ Centralizar a regra da invariante dentro de um único método synchronized do próprio objeto garante, sozinho, que a invariante nunca será violada, mesmo que a classe tenha outros métodos públicos que também alterem o mesmo atributo sem essa palavra-chave.
NotaO Construtor como Base da Indução
  • □ Numa classe cujo único meio de criação para o mundo externo é um método estático de fábrica (Produto.criar(...)), que internamente chama um construtor private sem nenhuma validação, a responsabilidade de servir como “base” da indução passa a ser do método de fábrica, e não do construtor em si.
  • □ Um construtor permissivo, que aceita qualquer dado, é uma boa prática para evitar exceções.
  • □ Lançar uma exceção no construtor impede que um “Objeto Zumbi” passe a existir na Heap.
  • □ Numa classe totalmente imutável — todos os atributos final, sem nenhum método que altere o estado depois da construção —, o “passo indutivo” da prova por indução é trivialmente satisfeito, pois não existe nenhuma transição capaz de levar o objeto para fora do conjunto \(V\) depois do nascimento.
NotaCQS: Comandos vs. Consultas
  • □ O construtor de uma classe, por ser responsável por estabelecer o estado inicial do objeto, deve ser classificado como um Comando dentro da disciplina de CQS, já que ele também determina o estado do objeto.
  • □ Um método como StringBuilder.append(String s), que muda o estado do objeto e também devolve a própria instância (return this;) para permitir encadeamento de chamadas (sb.append(a).append(b)), viola a recomendação estrita do CQS de que um Comando deve retornar void.
  • □ Uma Consulta que lança uma exceção quando chamada em condições inválidas — como Iterator.next(), que lança NoSuchElementException se chamado sem checar hasNext() antes — deixa de ser uma Consulta e passa a se comportar como um Comando, já que alterou o fluxo normal do programa.
  • □ Chamar uma Consulta livre de efeitos colaterais um milhão de vezes em sequência deixa o estado observável do objeto exatamente igual a como estava antes da primeira chamada.
NotaDesign by Contract
  • □ Um método que não tem nenhuma verificação de validade no início do seu corpo (nenhum if de guarda) ainda possui uma pré-condição no sentido do Design by Contract — ela apenas é a condição trivial “verdadeiro para qualquer entrada e qualquer estado”.
  • □ Se uma pré-condição falha, a responsabilidade recai sobre quem escreveu a classe, não sobre o chamador.
  • □ Um método processarSaque(double valor) que não contém nenhuma instrução assert no seu corpo não está sujeito a nenhuma pós-condição, dentro da disciplina de Design by Contract.
  • □ Num método transferir(ContaBancaria destino, double valor) que debita da própria conta e credita na conta destino, se ele não verifica que destino é diferente de this, e um chamador passa a própria conta como destino (contaA.transferir(contaA, 100)), quebrando a invariante de saldo, a culpa é do autor do método transferir, não de quem o chamou.
NotaFail-Fast e Exceções Padrão
  • □ Diferente de um código de erro -1 em C, uma exceção checked em Java (como IOException, que exige throws na assinatura do método) não pode ser silenciosamente ignorada pelo chamador, porque o compilador obriga o tratamento do erro.
  • IllegalArgumentException deve ser usada quando o objeto está no estado errado para a operação.
  • □ Se um método recebe, ao mesmo tempo, um argumento inválido (ex.: quantidade negativa) e é chamado num objeto que também está no estado errado para a operação (ex.: um carrinho já finalizado), a linguagem Java exige que a checagem de estado (IllegalStateException) seja sempre feita antes da checagem do argumento (IllegalArgumentException).
  • □ Chamar Objects.requireNonNull(cliente, "...") na última linha do corpo de um método, depois de todas as outras mutações de estado já terem sido executadas, cumpre igualmente bem o espírito do Fail-Fast, desde que a exceção ainda seja lançada antes do método retornar o controle ao chamador.
NotaIdentidade Física vs. Lógica
  • □ Para dois objetos Integer criados por autoboxing com o mesmo valor pequeno (Integer a = 100; Integer b = 100;), o operador == se comporta exatamente como no exemplo de Produto com new — sempre resulta em false, porque cada variável aponta para um objeto Integer diferente na Heap.
  • □ Ao contrário de comparar duas instâncias de Produto, comparar dois valores do tipo enum Estado (como no exemplo Pedido do início da aula) com == — por exemplo, this.status == Estado.PAGO — é seguro e dá o mesmo resultado que usar .equals(), porque a JVM garante que cada constante do enum existe como uma única instância na Heap.
  • □ Se um programador sobrescrever equals() de Produto para sempre devolver true, independentemente do objeto comparado, mas não sobrescrever hashCode() (mantendo o padrão herdado de Object, baseado no endereço), um HashSet<Produto> vai tratar dois Produto diferentes, adicionados nele, como duplicados.
  • □ Um TreeSet<Produto> configurado com um Comparator que ordena por SKU sofre exatamente do mesmo problema de aceitar “duplicatas” de negócio que um HashSet<Produto> sem equals()/hashCode() sobrescritos, pois ambas as coleções dependem exclusivamente de equals() para decidir se dois elementos são o mesmo.
NotaO Contrato equals()/hashCode()
  • □ Pelo mesmo contrato de equals()/hashCode(), se dois objetos Produto têm o mesmo hashCode(), então equals() entre eles deve obrigatoriamente devolver true.
  • □ O hashCode() pode usar atributos completamente diferentes dos usados pelo equals(), sem risco.
  • □ Se um Produto com equals()/hashCode() mal implementados for usado apenas como VALOR (não como chave) dentro de um HashMap<Integer, Produto>, o mesmo risco de “desaparecimento silencioso” descrito na aula para chaves se aplica igualmente a ele.
  • □ IDEs modernas geram os dois métodos juntos justamente para preservar essa coerência.