Soluções — O Objeto como Máquina de Estados
Aula 2 — Programação Orientada a Objetos
Dica(Resposta) Teste 1 — Estado, Comportamento e Identidade (revisitados)
- ✗ Falso — O Estado é o conjunto de valores dos atributos num dado instante; mesmo que esse conjunto seja vazio (zero campos), ele ainda é um conjunto bem definido — só que trivial e constante. “Não ter atributos” corresponde a uma máquina de um único estado (sem transições possíveis), não à ausência de Estado. O aluno que confunde “estado invariável” com “inexistência de estado” está tratando a cardinalidade do espaço de estados como se fosse a própria noção de Estado.
- ✔ Verdadeiro — A API de reflection permite obter um
Field, chamarsetAccessible(true)e escrever diretamente no atributo, ignorando qualquer método da classe. Isso comprova que o encapsulamento em Java é uma garantia de nível de linguagem/API (respeitada pelo compilador e pelo acesso “normal” ao código), não uma restrição absoluta e inquebrável imposta pela JVM em tempo de execução. Quem responde que reflection “não deveria funcionar porque o campo é private” confunde a barreira de compilação com uma barreira de execução. - ✗ Falso — Estado (o conjunto de valores dos atributos) e Identidade (a referência/endereço na Heap que distingue um objeto de outro) são conceitos ortogonais. Dois objetos
Produto("TV", 50.0)criados por doisnewdistintos têm Estado idêntico, mas são duas instâncias diferentes, em endereços diferentes — mudar o Estado de um não afeta o outro. Quem responde Verdadeiro está tratando “igualdade de estado” (o queequals()compara) como se fosse “igualdade de identidade” (o que==compara), a mesma confusão que motiva a distinçãoequals()/==em Java. - ✔ Verdadeiro — No instante em que
newexecuta, a JVM aloca um espaço na Heap e atribui a esse espaço um endereço/referência único, que passa a ser a Identidade do objeto por toda a sua vida — mesmo que, depois, seu Estado mude completamente (todos os atributos sejam reescritos), a Identidade original persiste até o objeto se tornar inalcançável. É esse instante de alocação, não qualquer atributo ou valor, que fixa a Identidade.
Dica(Resposta) Teste 2 — Struct Passivo vs. Agente Ativo
- ✗ Falso — Esta é exatamente a descrição do que ocorre em Java (classe = dados + métodos), atribuída erradamente a C. Em C, a
structsó agrupa dados; as funções que a manipulam são declaradas em outro lugar, sem nenhum vínculo sintático com o tipo. Confundir os dois paradigmas é o erro central que a aula tenta prevenir: achar que “agrupar campos” já é, por si, o mesmo que “encapsulamento forte”. - ✗ Falso —
staticem C só restringe a linkagem da função a uma unidade de tradução (o próprio arquivo) — não cria nenhum vínculo do tipo “esta função pertence a este tipo” como uma classe Java tem com seus métodos. Continua não havendo nenhum mecanismo de linguagem que impeça outro código do mesmo arquivo de manipular os campos da struct diretamente, nem a struct passa a poder recusar uma mutação inválida. Restringir a visibilidade textual não é o mesmo que encapsulamento orientado a objetos. - ✗ Falso —
privatesó bloqueia o acesso direto ao campo vindo de fora da classe — não valida nada por si só. Se o método público que altera o atributo não contiver nenhumifde guarda, qualquer chamada (mesmo com dado absurdo) passa livremente através desse método. A privacidade do campo é necessária, mas não suficiente: a recusa da mutação vem da lógica de validação escrita dentro do método, não da palavra-chaveprivate. - ✗ Falso — Encapsulamento forte é uma garantia imposta pela linguagem, não uma disciplina de equipe. Se os atributos são
public, nada no compilador ou na JVM impede qualquer código de alterá-los diretamente, ignorandopagar()por completo — basta um único desenvolvedor não seguir a convenção (ou um bug, ou uma biblioteca de terceiros) para que a garantia desapareça. “Todos concordam em não fazer isso” é o oposto do que o encapsulamento existe para eliminar: a necessidade de confiar na boa vontade de quem usa a classe.
Dica(Resposta) Teste 3 — O Modelo de Domínio Anêmico
- ✔ Verdadeiro — O defeito do Modelo Anêmico não é ter setters especificamente — é a ausência de regra de negócio dentro da classe, a inteligência ter “vazado” para fora. Uma classe que só expõe dados sem nenhum método que decida algo por si (como
pagar(),vender()) continua sendo uma “struct glorificada”, mesmo sendo imutável e só de leitura. Quem responde Falso está confundindo o mecanismo mais citado do anti-padrão (setter cego) com sua causa real (zero comportamento). - ✗ Falso — “Flexibilidade” aqui é uma armadilha de vocabulário: dar a quem consome a classe liberdade total para ler e escrever qualquer atributo não é riqueza de design, é justamente a ausência de proteção que a aula chama de anti-padrão. Essa “flexibilidade” empurra para fora da classe a responsabilidade de manter a consistência dos dados — o oposto do que um bom design faz. O termo tecnicamente correto para essa característica (“Modelo Rico de Domínio”) é o antônimo do Modelo Anêmico, não um sinônimo dele.
- ✗ Falso — No padrão Builder, os setters intermediários são deliberadamente “cegos” porque o objeto ainda está incompleto, em processo de montagem — a invariante é garantida no ponto certo,
build(), que só devolve o objeto final se ele for válido. Isso é estruturalmente diferente do Modelo Anêmico, em que a entidade já “viva” no sistema aceita qualquer mutação a qualquer momento de sua vida útil, sem nenhum ponto central de validação. O Builder é uma técnica legítima de construção incremental; o Modelo Anêmico é um anti-padrão porque a entidade nunca protege sua própria invariante, nem na criação nem depois. - ✔ Verdadeiro — Contrastando
Pedido.pagar()(que decide internamente se a transição é legal) comPedidoAnemico.setStatus(String s)(que aceita qualquer string sem julgamento), fica claro que nomear o método pela intenção de negócio, em vez de pela mecânica de atribuição, é o que preserva a capacidade do objeto de guardar suas próprias regras. Um setter genérico delega a decisão para quem chama; um método de intenção mantém a decisão dentro do objeto.
Dica(Resposta) Teste 4 — A Máquina de Estados Finita (DFA)
- ✔ Verdadeiro — Determinismo e finitude são propriedades independentes: determinismo depende só de a função de transição ser unívoca (um único próximo estado por par estado-evento), não do tamanho do conjunto de estados. Um autômato com infinitos estados, mas transição sempre única, seria determinístico — só deixaria de ser um DFA no sentido estrito, porque a definição de “Finite Automaton” exige, além do determinismo, que o conjunto de estados seja finito. O erro comum aqui é achar que “determinístico” e “finito” são a mesma exigência.
- ✔ Verdadeiro — O diagrama de estados do
Produtosó definevender(x<all)(permanece em DISPONÍVEL) evender(all)(vai para ESGOTADO); vender mais do que o estoque atual não tem nenhum destino previsto pela máquina. Como um DFA exige que toda combinação (estado, evento) leve a exatamente um estado, e essa combinação específica não tem destino legal no domínio de negócio, o código precisa recusar a chamada (IllegalStateException) em vez de deixar a máquina “vazar” para um estado matematicamente impossível (estoque negativo). - ✗ Falso — É exatamente o oposto da definição de determinismo: um DFA bem definido garante que, para cada par (estado, evento) — inclusive nos casos de borda, não só nos “normais” — existe exatamente um próximo estado conhecido. Não há espaço para comportamento “mágico”; se um caso de borda não tem transição definida, a resposta correta é recusar a operação (Fail-Fast), não improvisar um resultado imprevisível.
- ✔ Verdadeiro — Assim como uma invariante de laço permite provar a corretude de um algoritmo por indução (inicialização, manutenção, término), o determinismo de uma máquina de estados garante que o comportamento do sistema para qualquer sequência de eventos é previsível e único — pré-requisito para qualquer argumento de prova formal. Se a mesma combinação (estado, evento) pudesse levar a resultados diferentes, não haveria uma única “trajetória” a se provar correta.
Dica(Resposta) Teste 5 — A Máquina de Estados Encapsulada em Java
- ✔ Verdadeiro — O mapeamento “atributos privados = estado” da aula é sobre atributos de instância, que pertencem a cada objeto individualmente. Um campo
staticvive associado à classe como um todo, compartilhado por todas as instâncias — mudar seu valor não corresponde à transição de estado de um objeto específico, é uma variável fora do escopo da máquina de estados de qualquer instância isolada. - ✗ Falso — Getters são Consultas (por CQS, sem efeito colateral); se não existe nenhum Comando público, nenhuma transição está disponível depois da construção. O objeto nasce em um estado e fica congelado nele para sempre — a máquina, na prática, tem um único estado alcançável (o de nascimento), mesmo que a classe tenha múltiplos atributos que “poderiam” assumir outros valores em teoria.
- ✗ Falso — Mesmo com o campo
private, devolver a referência real do objeto mutável pelo getter permite que qualquer código externo chame.add()/.remove()na lista devolvida e altere o estado interno diretamente, sem passar por nenhuma transição validada da classe.privateprotege a variável (a referência), não o objeto apontado por ela — é preciso devolver uma cópia ou uma view imutável para preservar de fato a garantia da máquina de estados. - ✗ Falso — O encapsulamento não é sobre de onde vem o dado (arquivo, rede, outro objeto) — é sobre se o método que recebe o dado valida a transição antes de aplicá-la. Um
restaurarEstadoque copia tudo sem checar nada é estruturalmente idêntico a um setter cego: qualquer dado corrompido vindo de fora entra direto no objeto e produz um “Objeto Zumbi”, exatamente o problema que o Fail-Fast e a máquina de estados existem para evitar.
Dica(Resposta) Teste 6 — Invariantes de Classe
- ✔ Verdadeiro — A garantia de invariante (base da indução + passo indutivo) é sobre os pontos observáveis de fora da classe: o instante logo após o construtor e o instante logo após cada método retornar. Internamente, um método pode passar por estados intermediários inconsistentes (ex.: debitar de uma conta antes de creditar em outra, num método de transferência) — o que importa é que, quando o controle volta ao chamador, a invariante já esteja restabelecida.
- ✗ Falso — O fato de o total ser uma propriedade derivada (resultado de somar os itens, não uma entrada livre) não elimina a necessidade de proteção — pelo contrário, é exatamente por isso que ela não deveria ter um
setTotal(valor): a única forma segura de garantir “total = soma dos itens” é que a própria classe recalcule/atualize o total dentro do método de transição oficial (adicionarItem). Achar que “é derivada” significa “não precisa de invariante” inverte a lógica: é precisamente a invariante que define o que “derivada” quer dizer aqui. - ✗ Falso — Mesmo com um único módulo acessando o campo hoje, a invariante continua sem nenhuma proteção estrutural: nada no código impede que um segundo módulo passe a acessar
saldodiretamente no futuro, nem impede que o próprio módulo único tenha, em algum caminho interno, um ponto que esqueça de checar a regra. “Só um lugar checando hoje” não é o mesmo que “impossível de violar” — a proteção real só existe quando a regra vive dentro do objeto, não na disciplina (atual e temporária) de quem o usa. - ✗ Falso —
synchronizedsó serializa chamadas ao método que a declara; se existir qualquer outro método público que também altere o mesmo atributo semsynchronized, duas threads podem entrelaçar suas execuções por esse outro caminho e quebrar a invariante mesmo assim. Centralizar a regra ajuda, mas só protege de fato se todos os pontos de mutação do atributo passarem pelo mesmo mecanismo de exclusão — não basta proteger um único método e deixar outro caminho de mutação aberto.
Dica(Resposta) Teste 7 — O Construtor como Base da Indução
- ✔ Verdadeiro — O papel de “base da indução” pertence a qualquer ponto de entrada pelo qual um objeto nasce e que seja efetivamente alcançável de fora da classe. Se o construtor é
privatee não valida nada, e a única via pública é o método de fábrica, é o corpo desse método de fábrica que precisa fazer a validação Fail-Fast antes de chamar o construtor — do contrário, a garantia \(o \in V\) no nascimento simplesmente não existe em lugar nenhum do código. - ✗ Falso — É exatamente o cenário do “Objeto Zumbi”: um construtor permissivo não evita problemas, apenas os posterga e os torna mais difíceis de rastrear, porque o erro vai aparecer distante da causa real, num método qualquer que assumia (sem verificar de novo) que o objeto tinha nascido válido. “Evitar exceções” trocando-as por dados corrompidos silenciosos é uma troca ruim: o construtor deve recusar ativamente, não aceitar qualquer coisa.
- ✔ Verdadeiro — Se o construtor lança uma exceção Fail-Fast diante de dados inválidos, a construção do objeto é interrompida — a referência nunca chega a existir de forma utilizável pelo restante do programa. É exatamente o comportamento inverso do construtor permissivo (que cria o objeto mesmo com dados ruins, gerando o zumbi); a exceção é o mecanismo que garante a base da indução (\(o \in V\)) ao custo de nunca permitir que \(o \notin V\) exista.
- ✔ Verdadeiro — O passo indutivo exige mostrar que todo método público, aplicado a um objeto em \(s_n \in V\), resulta em \(s_{n+1} \in V\). Se não existe nenhum método capaz de mutar o estado (todos os atributos são fixados no construtor e nunca mais alterados), o conjunto de “métodos que poderiam violar a invariante” é vazio, e a implicação é satisfeita por vacuidade — toda a responsabilidade de manter a validade do objeto recai inteiramente sobre a base (o construtor), sem nenhum risco introduzido depois.
Dica(Resposta) Teste 8 — CQS: Comandos vs. Consultas
- ✗ Falso — CQS classifica métodos chamados sobre um objeto já existente, decidindo entre “mudar o estado observável” (Comando) e “devolver um dado sem efeito colateral” (Consulta). O construtor não se encaixa nessa dicotomia porque não há um estado anterior a preservar ou consultar — ele é a base da indução, uma categoria à parte, responsável por levar o objeto de “não-existente” para “existente e válido”, não uma transição entre dois estados já dentro de \(V\).
- ✔ Verdadeiro — Pela definição estrita de CQS apresentada na aula, um Comando deve sinalizar “isto muda o mundo” retornando
void;append()muda o estado (adiciona ao buffer) e ainda devolve um valor (this), misturando os dois papéis. Na prática, esse é um desvio deliberado e amplamente aceito (padrão fluent interface/encadeamento de métodos), mas continua sendo, por definição, uma violação da regra estrita ensinada aqui — o valor devolvido não é um dado de consulta independente do estado, é a própria referência mutada. - ✗ Falso — O critério de CQS não é “o método pode alterar o fluxo de controle”, é “o método pode alterar o estado observável do objeto”. Lançar uma exceção ao detectar uma pré-condição violada é o comportamento Fail-Fast esperado de qualquer método (Comando ou Consulta) — não muda saldo, estoque ou qualquer outro atributo de negócio do objeto.
Iterator.next()continua sendo uma Consulta: ela só se recusa a devolver um dado quando não há dado válido para devolver, o que é diferente de mutar o estado. - ✔ Verdadeiro — Por definição, uma Consulta em conformidade com CQS não tem nenhum efeito colateral sobre o estado do objeto; se cada chamada individual não altera nada, repetir a chamada qualquer número de vezes (uma ou um milhão) não pode acumular nenhuma mudança, porque não há nenhuma mudança para acumular. Se o resultado dependesse de quantas vezes a Consulta foi chamada, ela já teria efeito colateral e deixaria de ser uma Consulta.
Dica(Resposta) Teste 9 — Design by Contract
- ✔ Verdadeiro — No formalismo de Design by Contract, toda operação tem uma pré-condição, mesmo que implícita; a ausência de um
ifde guarda no código não significa “sem pré-condição”, significa que a pré-condição escolhida pelo autor da classe é a mais permissiva possível (aceita qualquer chamada). Confundir “nenhuma verificação escrita” com “nenhum contrato de entrada” é um erro comum — o contrato sempre existe, só varia em quão restritivo ele é. - ✗ Falso — É a inversão exata da regra ensinada: 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. Quem escreveu a classe só é responsável por garantir a pós-condição, uma vez que as pré-condições foram satisfeitas. Trocar as duas direções de responsabilidade é o erro conceitual central de Design by Contract: o contrato tem dois lados, e cada lado responde por uma metade dele.
- ✗ Falso — A pós-condição é a promessa de design de que, satisfeitas as pré-condições, o método deixa o objeto num estado onde as invariantes seguem válidas; essa promessa existe independentemente de o código conter um
assertexplícito ou não. Oassertdo exemplo da aula é só uma ferramenta de verificação/documentação em tempo de execução — removê-lo não remove a obrigação de a lógica do método efetivamente preservar a invariante, só remove a checagem automática que denunciaria a falha. - ✔ Verdadeiro — Pela disciplina de Design by Contract, toda restrição necessária para a pós-condição se sustentar precisa estar coberta por uma pré-condição explícita e checada;
destino != thisé exatamente esse tipo de restrição implícita que o método deveria ter validado (Fail-Fast) e não validou. Como o método não declarou nem checou essa pré-condição, o chamador não tinha como saber que aquela chamada era ilegal — a responsabilidade recai sobre quem escreveutransferir, por deixar uma lacuna no contrato.
Dica(Resposta) Teste 10 — Fail-Fast e Exceções Padrão
- ✗ Falso — O compilador só obriga o chamador a escrever uma cláusula
catch(ou repassar comthrows) sintaticamente; nada impede umcatch (IOException e) {}vazio, que descarta o erro exatamente como oif (resultado == -1)que ninguém verificou em C. “Ser forçado a escrevercatch” não é o mesmo que “ser forçado a tratar o erro de verdade” — o silêncio ainda é possível, só fica um pouco mais visível no código-fonte. - ✗ Falso — É a troca clássica das duas exceções:
IllegalArgumentExceptioné sobre a carga (um argumento que não faz sentido no domínio, independentemente do estado do objeto);IllegalStateExceptioné sobre o momento (o dado está correto, mas o objeto não está no estado certo para aceitar essa operação agora). A afirmação usa o jargão certo, mas atribui cada exceção à situação da outra — o tipo de erro que soa plausível para quem decorou os nomes sem entender a distinção carga vs. momento. - ✗ Falso — Java não impõe nenhuma ordem entre as duas checagens; qual verificação vem primeiro no corpo do método é uma decisão de design de quem escreve a classe (geralmente documentada no Javadoc), não uma regra da linguagem ou da JVM. Nada impede o autor de checar o argumento antes do estado, ou vice-versa — o importante é que ambas sejam feitas antes de qualquer mutação (Fail-Fast), não a ordem relativa entre elas.
- ✗ Falso — O espírito do Fail-Fast é interromper o fluxo antes de qualquer mutação de estado acontecer, não simplesmente “antes do método retornar”. Se as mutações já foram executadas quando o
requireNonNullfinalmente dispara no final do método, o objeto pode ter ficado num estado parcialmente alterado e inconsistente até esse ponto — a exceção eventualmente é lançada, mas tarde demais para evitar o dano; a checagem precisa vir antes de qualquerthis.campo = ..., não só antes doreturn.
Dica(Resposta) Teste 11 — Identidade Física vs. Lógica
- ✗ Falso — Diferente de
new Produto(...), que sempre aloca uma instância nova, o autoboxing de valores inteiros pequenos (entre -128 e 127) reaproveita instâncias de um cache interno (Integer.valueOf);aebacabam apontando para o mesmo objetoIntegercacheado, entãoa == bresulta emtrue— não porque==tenha deixado de comparar referências, mas porque, nesse caso específico, as duas referências realmente coincidem. É um contraexemplo clássico de como a suposição “cada variável tem seu próprio objeto” pode falhar silenciosamente. - ✔ Verdadeiro — Diferente de uma classe comum, onde cada
newcria uma instância física distinta (por isso==costuma ser perigoso), as constantes de umenumsão inicializadas pela JVM uma única vez cada, como singletons; toda referência aEstado.PAGOno programa aponta para o mesmo objeto na Heap. Por isso,==entre valores de enum é seguro e é, aliás, a forma idiomática recomendada em Java — aqui identidade física e identidade lógica coincidem sempre, ao contrário doProdutodo exemplo do SKU. - ✗ Falso — Um
HashSetprimeiro usahashCode()para decidir em qual “balde” (bucket) colocar/procurar o objeto, e só chamaequals()para comparar objetos que já caíram no mesmo balde. ComohashCode()continua sendo o padrão deObject(baseado no endereço físico), doisProdutodiferentes praticamente certamente caem em buckets diferentes, eequals()(que sempre devolveriatrue) nunca chega a ser chamado para compará-los entre si — oHashSetos trata como distintos mesmo assim. É o contrato quebrado na direção oposta à usual:equals()diz que são iguais, mashashCode()não colabora, e o resultado prático é que oSet“não percebe” a igualdade. - ✗ Falso — Diferente de
HashSet, que depende do parequals()/hashCode(), umTreeSetdecide duplicidade usando exclusivamente oComparator(ouComparable) fornecido: dois elementos são considerados “iguais” para fins do conjunto quandocompare(a, b) == 0, mesmo queequals()nunca tenha sido sobrescrito. UmComparatorpor SKU já rejeitaria corretamente um segundoProdutocom o mesmo SKU, sem que a classe precisasse tocar emequals()/hashCode()— uma via alternativa de resolver o mesmo problema de identidade lógica.
Dica(Resposta) Teste 12 — O Contrato equals()/hashCode()
- ✗ Falso — O contrato só garante a implicação numa direção:
equals()verdadeiro implicahashCode()igual. A volta não vale: dois objetos podem ter o mesmohashCode()(uma colisão de hash, esperada e permitida) sem seremequals()— é exatamente por isso que umHashMap/HashSet, depois de encontrar o balde certo pelo hash, ainda precisa chamarequals()para confirmar se os objetos são de fato o mesmo, em vez de confiar só na coincidência do hash. - ✗ Falso — É a inversão direta da regra do contrato (“o hash deve olhar apenas para os campos do equals”) e da analogia do armazém: se
equals()compara o SKU mashashCode()usa outro campo (a cor da embalagem, por exemplo), dois objetos considerados iguais porequals()podem cair em corredores/baldes diferentes — o sistema procura no corredor errado e afirma, silenciosamente, que o item não existe. Usar campos diferentes nos dois métodos quebra a consistência exigida pelo contrato e não é uma escolha “sem risco”. - ✗ Falso — O
HashMapsó consultahashCode()/equals()do objeto usado como chave, para decidir em qual balde procurar e confirmar a identidade durante a busca; o valor é apenas armazenado e devolvido junto da chave correspondente, sem que seu próprioequals()/hashCode()participe da mecânica de localização. UmProdutocom esses métodos mal implementados, usado como valor, continua sendo recuperado corretamente pela chaveInteger— o problema do “corredor errado” só existe quando o objeto com o contrato quebrado é a própria chave da busca. - ✔ Verdadeiro — IDEs como IntelliJ IDEA e Eclipse oferecem geração automática de
equals()/hashCode()a partir dos mesmos campos escolhidos pelo programador, precisamente para evitar o erro de escrever um sem o outro (ou de escrevê-los usando conjuntos de campos diferentes). Essa é uma aplicação prática, fora do escopo direto da aula, da mesma lição da analogia do armazém: os dois métodos formam um contrato indissociável, e ferramentas de desenvolvimento institucionalizaram essa disciplina para reduzir o erro humano.